phase8: full manuscript (12 sections + appendix), verified bibliography, tables/figures; compiles to 44 pages
43 changed files +1,302 −374
added
Makefile
+42 −0
@@ -0,0 +1,42 @@ | ||
| 1 | +# WP37 — reproducible pipeline (run from the project root; requires the authorised snapshot in data/raw/) | |
| 2 | +PY := .venv/bin/python | |
| 3 | +.PHONY: all env ingest panel match validate phase1 phase4 phase5 robust tables figures paper test clean | |
| 4 | + | |
| 5 | +all: ingest panel match validate phase4 phase5 robust tables figures paper | |
| 6 | + | |
| 7 | +env: | |
| 8 | + uv venv --python 3.12 .venv && uv pip install --python $(PY) -e . | |
| 9 | + | |
| 10 | +ingest: | |
| 11 | + $(PY) -m src.ingestion.load_snapshot && $(PY) -m src.ingestion.statcan | |
| 12 | + | |
| 13 | +panel: | |
| 14 | + $(PY) -m src.cleaning.retailers && $(PY) -m src.cleaning.build_panel | |
| 15 | + | |
| 16 | +match: | |
| 17 | + $(PY) -m src.matching.calibrate_thresholds && $(PY) -m src.matching.match_products && $(PY) -m src.matching.recall_gtin | |
| 18 | + | |
| 19 | +validate: # LLM second annotation requires ANTHROPIC_API_KEY in .env; operator labels are kept in the CSV | |
| 20 | + $(PY) -m src.matching.validation_report && $(PY) -m src.cleaning.coverage && $(PY) -m src.reports.phase1_audit | |
| 21 | + | |
| 22 | +phase4: | |
| 23 | + $(PY) -m src.econometrics.build_matched_panel && $(PY) -m src.econometrics.model_a_dispersion && $(PY) -m src.econometrics.model_b_promotions \ | |
| 24 | + && $(PY) -m src.econometrics.model_c_spillovers && $(PY) -m src.indices.price_indices && $(PY) -m src.indices.basket_optimization && $(PY) -m src.forecasting.rolling_forecast | |
| 25 | + | |
| 26 | +phase5: | |
| 27 | + $(PY) -m src.indices.qgpai | |
| 28 | + | |
| 29 | +robust: | |
| 30 | + $(PY) -m src.econometrics.robustness | |
| 31 | + | |
| 32 | +tables figures: | |
| 33 | + $(PY) -m src.reports.make_tables && $(PY) -m src.reports.make_figures | |
| 34 | + | |
| 35 | +paper: | |
| 36 | + cd paper && latexmk -pdf -interaction=nonstopmode main.tex | |
| 37 | + | |
| 38 | +test: | |
| 39 | + $(PY) -m pytest -q | |
| 40 | + | |
| 41 | +clean: | |
| 42 | + cd paper && latexmk -C | |
added
README.md
+40 −0
@@ -0,0 +1,40 @@ | ||
| 1 | +# WP37 — The Hidden Architecture of Grocery Prices: Promotions, Retail Competition, and the Cost of Living in Quebec | |
| 2 | + | |
| 3 | +**UQO Working Paper No. 37** — Simon-Pierre Boucher, Département des sciences administratives, Université du Québec en Outaouais. Version 1.0, November 2026. | |
| 4 | + | |
| 5 | +Reproducible research pipeline built on a **read-only snapshot** of the Food-Ka database (owner export, 2026-11-01). Nothing in production was modified; the raw snapshot is not committed (`data/raw/README.md` documents provenance and checksum). | |
| 6 | + | |
| 7 | +## Layout | |
| 8 | +``` | |
| 9 | +CLAUDE.md mandate (verbatim) | |
| 10 | +config/settings.yaml paths, seed (20261101), snapshot identity, scope | |
| 11 | +config/basket.yaml preregistered 30-item basket (frozen) | |
| 12 | +research/preregistration.md frozen hypotheses H1–H6, estimands, analysis plan (sha256 in research/decisions.md) | |
| 13 | +research/decisions.md decision log and amendments A-001 … A-003 | |
| 14 | +src/ingestion snapshot → parquet; Statistics Canada WDS benchmarks | |
| 15 | +src/cleaning unit normalisation, retailer hierarchy, daily/weekly panel reconstruction, coverage | |
| 16 | +src/matching tiered cross-banner matching, UPC calibration, LLM second annotation, validation | |
| 17 | +src/econometrics matched panels, Model A (dispersion), B (promotions), C (spillovers), robustness | |
| 18 | +src/indices Model D (indices), Model E (basket MIP), QGPAI | |
| 19 | +src/forecasting Model F (rolling-origin forecasts) | |
| 20 | +src/reports Phase 1 audit, LaTeX tables, figures | |
| 21 | +literature/ seed references, Crossref-verified metadata, evidence matrix | |
| 22 | +experiments/ JSON summaries of every model | |
| 23 | +paper/ main.tex, sections/, tables/, figures/, references.bib, main.pdf | |
| 24 | +reports/ phase0_feasibility, phase1_data_audit, referee_1, referee_2, limitations, reproducibility | |
| 25 | +logs/ run ledger (md + jsonl), manifests with SHA-256 of outputs, pipeline log | |
| 26 | +tests/ pytest (units, panel invariants, matching rules, output consistency) | |
| 27 | +``` | |
| 28 | + | |
| 29 | +## Reproduce | |
| 30 | +``` | |
| 31 | +make env # uv venv (Python 3.12) + pinned deps (requirements-lock.txt) | |
| 32 | +make ingest panel match validate phase4 phase5 robust tables figures paper | |
| 33 | +make test | |
| 34 | +``` | |
| 35 | +The LLM second annotation (`src/matching/llm_annotate.py`) needs `ANTHROPIC_API_KEY` in `.env`; its labels are stored in `data/processed/match_validation_sample.csv` so the rest of the pipeline runs without API access. Total LLM cost of the project: 1.21 USD (three annotation runs, logged in `logs/run_ledger.jsonl`). | |
| 36 | + | |
| 37 | +## Key design decisions (see reports/phase0_feasibility.md and reports/phase1_data_audit.md) | |
| 38 | +- Posted price = weekly duration-weighted modal price (store-mixture noise in anti-bot storefront feeds). | |
| 39 | +- Primary matching tiers A (UPC) + B (brand + package + identical token set); C and D in sensitivity only. | |
| 40 | +- No store geography, no regular-price history, no quantities: H4 geography and QGPAI component 5 are explicitly not computed. | |
modified
literature/manual_references.yaml
+2 −0
@@ -7,3 +7,5 @@ | ||
| 7 | 7 | - {key: competitionbureau2023, topic: canada, doi: null, url: "https://competition-bureau.canada.ca/en/how-we-foster-competition/education-and-outreach/canada-needs-more-grocery-competition", author: "{Competition Bureau Canada}", title: "Canada Needs More Grocery Competition: Retail Grocery Market Study Report", year: 2023, type: report} |
| 8 | 8 | - {key: cakir2021, topic: shrinkflation, doi: "10.1002/agr.21724"} |
| 9 | 9 | - {key: hoppe2024, topic: shrinkflation, doi: "10.1093/icc/dtae035"} |
| 10 | +- {key: chevalier2003, topic: sales, doi: "10.1257/000282803321455142"} | |
| 11 | +- {key: hottman2016, topic: substitution, doi: "10.1093/qje/qjw012"} | |
modified
literature/references_verified.csv
+4 −2
@@ -19,7 +19,6 @@ klenow2008,rigidity,Klenow Kryvtsov,State-Dependent or Time-Dependent Pricing: D | ||
| 19 | 19 | eichenbaum2011,rigidity,Eichenbaum Jaimovich Rebelo,"Reference Prices, Costs, and Nominal Rigidities",2011,True,10.1257/aer.101.1.234,"Reference Prices, Costs, and Nominal Rigidities",American Economic Review,101.0,1,234-262,2011.0,journal-article,American Economic Association,"Eichenbaum, Martin; Jaimovich, Nir; Rebelo, Sergio",1.0, |
| 20 | 20 | kehoe2015,rigidity,Kehoe Midrigan,Prices are sticky after all,2015,True,10.1016/j.jmoneco.2014.12.004,Prices are sticky after all,Journal of Monetary Economics,75.0,,35-53,2015.0,journal-article,Elsevier BV,"Kehoe, Patrick; Midrigan, Virgiliu",1.0, |
| 21 | 21 | anderson2017,sales,Anderson Malin Nakamura Simester Steinsson,Informational rigidities and the stickiness of temporary sales,2017,True,10.1016/j.jmoneco.2017.06.003,Informational rigidities and the stickiness of temporary Sales,Journal of Monetary Economics,90.0,,64-83,2017.0,journal-article,Elsevier BV,"Anderson, Eric; Malin, Benjamin A.; Nakamura, Emi; Simester, Duncan; Steinsson, Jón",1.0, |
| 22 | −chevalier2003,sales,Chevalier Kashyap Rossi,Why Don't Prices Rise During Periods of Peak Demand? Evidence from Scanner Data,2003,True,10.2139/ssrn.319966,Why Don't Prices Rise During Periods of Peak Demand? Evidence from Scanner Data,,,,,2002.0,posted-content,Elsevier BV,"Rossi, Peter E.; Chevalier, Judith A.; Kashyap, Anil K.",1.0, | |
| 23 | 22 | chevalier2019,sales,Chevalier Kashyap,Best Prices: Price Discrimination and Consumer Substitution,2019,True,10.1257/pol.20150362,Best Prices: Price Discrimination and Consumer Substitution,American Economic Journal: Economic Policy,11.0,1,126-159,2019.0,journal-article,American Economic Association,"Chevalier, Judith A.; Kashyap, Anil K",1.0, |
| 24 | 23 | coibion2015,sales,Coibion Gorodnichenko Hong,"The Cyclicality of Sales, Regular and Effective Prices: Business Cycle and Policy Implications",2015,True,10.1257/aer.20121546,"The Cyclicality of Sales, Regular and Effective Prices: Business Cycle and Policy Implications",American Economic Review,105.0,3,993-1029,2015.0,journal-article,American Economic Association,"Coibion, Olivier; Gorodnichenko, Yuriy; Hong, Gee Hee",1.0, |
| 25 | 24 | golosov2007,menu_costs,Golosov Lucas,Menu Costs and Phillips Curves,2007,True,10.1086/512625,Menu Costs and Phillips Curves,Journal of Political Economy,115.0,2,171-199,2007.0,journal-article,University of Chicago Press,"Golosov, Mikhail; Lucas Jr., Robert E.",1.0, |
@@ -62,7 +61,6 @@ cakir2014,shrinkflation,Çakır Balagtas,Consumer response to package downsizing | ||
| 62 | 61 | einav2010,scanner,Einav Leibtag Nevo,Recording discrepancies in Nielsen Homescan data: Are they present and do they matter?,2010,True,10.1007/s11129-009-9073-0,Recording discrepancies in Nielsen Homescan data: Are they present and do they matter?,Quantitative Marketing and Economics,8.0,2,207-239,2009.0,journal-article,Springer Science and Business Media LLC,"Einav, Liran; Leibtag, Ephraim; Nevo, Aviv",1.0, |
| 63 | 62 | diewert2020,indices,Diewert Fox,Measuring Real Consumption and CPI Bias under Lockdown Conditions,2020,True,10.3386/w27144,Measuring Real Consumption and CPI Bias under Lockdown Conditions,,,,,2020.0,report,National Bureau of Economic Research,"Diewert, W. Erwin; Fox, Kevin",1.0, |
| 64 | 63 | beck2024,online,Beck Jaravel,Prices and Global Inequality: New Evidence from Worldwide Scanner Data,2020,True,10.2139/ssrn.3671980,Prices and Global Inequality: New Evidence from Worldwide Scanner Data,SSRN Electronic Journal,,,,2020.0,journal-article,Elsevier BV,"Beck, Günter W.; Jaravel, Xavier",1.0, |
| 65 | −hottman2016,substitution,Hottman Redding Weinstein,Quantifying the Sources of Firm Heterogeneity,2016,True,10.2139/ssrn.2693688,Quantifying the Sources of Firm Heterogeneity,,,,,2015.0,posted-content,Elsevier BV,"Hottman, Colin; Redding, Stephen J.; Weinstein, David E.",1.0, | |
| 66 | 64 | atkin2018,welfare,Atkin Faber Gonzalez-Navarro,Retail Globalization and Household Welfare: Evidence from Mexico,2018,True,10.1086/695476,Retail Globalization and Household Welfare: Evidence from Mexico,Journal of Political Economy,126.0,1,1-73,2018.0,journal-article,University of Chicago Press,"Atkin, David; Faber, Benjamin; Gonzalez-Navarro, Marco",1.0, |
| 67 | 65 | cavallo2023,online,Cavallo Kryvtsov,What can stockouts tell us about inflation? Evidence from online micro data,2023,True,10.1016/j.jinteco.2023.103769,What can stockouts tell us about inflation? Evidence from online micro data,Journal of International Economics,146.0,,103769,2023.0,journal-article,Elsevier BV,"Cavallo, Alberto; Kryvtsov, Oleksiy",1.0, |
| 68 | 66 | adams2024,retail,Adams Williams,Zone Pricing in Retail Oligopoly,2019,True,10.1257/mic.20170130,Zone Pricing in Retail Oligopoly,American Economic Journal: Microeconomics,11.0,1,124-156,2019.0,journal-article,American Economic Association,"Adams, Brian; Williams, Kevin R.",1.0, |
@@ -76,3 +74,7 @@ hortacsu2004,search,,"Product Differentiation, Search Costs, and Competition in | ||
| 76 | 74 | competitionbureau2023,canada,{Competition Bureau Canada},Canada Needs More Grocery Competition: Retail Grocery Market Study Report,2023,True,,Canada Needs More Grocery Competition: Retail Grocery Market Study Report,,,,,2023.0,report,,{Competition Bureau Canada},1.0,no DOI; verified via https://competition-bureau.canada.ca/en/how-we-foster-competition/education-and-outreach/canada-needs-more-grocery-competition |
| 77 | 75 | cakir2021,shrinkflation,,Retail pass‐through of package downsizing,2021,True,10.1002/agr.21724,Retail pass‐through of package downsizing,Agribusiness,38,2,259-278,2021.0,journal-article,Wiley,"Çakır, Metin",1.0,resolved by DOI |
| 78 | 76 | hoppe2024,shrinkflation,,"Hidden price increases, product downsizing, and shrinkflation: a post-Keynesian macroeconomic perspective",2024,True,10.1093/icc/dtae035,"Hidden price increases, product downsizing, and shrinkflation: a post-Keynesian macroeconomic perspective",Industrial and Corporate Change,34,2,318-341,2024.0,journal-article,Oxford University Press (OUP),"Melmiès, Jordan",1.0,resolved by DOI |
| 77 | +chevalier2003,sales,,Why Don't Prices Rise During Periods of Peak Demand? Evidence from Scanner Data,2003,True,10.1257/000282803321455142,Why Don't Prices Rise During Periods of Peak Demand? Evidence from Scanner Data,American Economic Review,93,1,15-37,2003.0,journal-article,American Economic Association,"Chevalier, Judith A; Kashyap, Anil K; Rossi, Peter E",1.0,resolved by DOI | |
| 78 | +hottman2016,substitution,,"Quantifying the Sources of Firm Heterogeneity | |
| 79 | + *",2016,True,10.1093/qje/qjw012,"Quantifying the Sources of Firm Heterogeneity | |
| 80 | + *",The Quarterly Journal of Economics,131,3,1291-1364,2016.0,journal-article,Oxford University Press (OUP),"Hottman, Colin J.; Redding, Stephen J.; Weinstein, David E.",1.0,resolved by DOI | |
modified
literature/seed_references.yaml
+0 −2
@@ -19,7 +19,6 @@ | ||
| 19 | 19 | - {key: eichenbaum2011, topic: rigidity, author: "Eichenbaum Jaimovich Rebelo", title: "Reference Prices, Costs, and Nominal Rigidities", year: 2011} |
| 20 | 20 | - {key: kehoe2015, topic: rigidity, author: "Kehoe Midrigan", title: "Prices are sticky after all", year: 2015} |
| 21 | 21 | - {key: anderson2017, topic: sales, author: "Anderson Malin Nakamura Simester Steinsson", title: "Informational rigidities and the stickiness of temporary sales", year: 2017} |
| 22 | −- {key: chevalier2003, topic: sales, author: "Chevalier Kashyap Rossi", title: "Why Don't Prices Rise During Periods of Peak Demand? Evidence from Scanner Data", year: 2003} | |
| 23 | 22 | - {key: chevalier2019, topic: sales, author: "Chevalier Kashyap", title: "Best Prices: Price Discrimination and Consumer Substitution", year: 2019} |
| 24 | 23 | - {key: coibion2015, topic: sales, author: "Coibion Gorodnichenko Hong", title: "The Cyclicality of Sales, Regular and Effective Prices: Business Cycle and Policy Implications", year: 2015} |
| 25 | 24 | - {key: golosov2007, topic: menu_costs, author: "Golosov Lucas", title: "Menu Costs and Phillips Curves", year: 2007} |
@@ -67,7 +66,6 @@ | ||
| 67 | 66 | - {key: einav2010, topic: scanner, author: "Einav Leibtag Nevo", title: "Recording discrepancies in Nielsen Homescan data: Are they present and do they matter?", year: 2010} |
| 68 | 67 | - {key: diewert2020, topic: indices, author: "Diewert Fox", title: "Measuring Real Consumption and CPI Bias under Lockdown Conditions", year: 2020} |
| 69 | 68 | - {key: beck2024, topic: online, author: "Beck Jaravel", title: "Prices and Global Inequality: New Evidence from Worldwide Scanner Data", year: 2020} |
| 70 | −- {key: hottman2016, topic: substitution, author: "Hottman Redding Weinstein", title: "Quantifying the Sources of Firm Heterogeneity", year: 2016} | |
| 71 | 69 | - {key: atkin2018, topic: welfare, author: "Atkin Faber Gonzalez-Navarro", title: "Retail Globalization and Household Welfare: Evidence from Mexico", year: 2018} |
| 72 | 70 | - {key: cavallo2023, topic: online, author: "Cavallo Kryvtsov", title: "What can stockouts tell us about inflation? Evidence from online micro data", year: 2023} |
| 73 | 71 | - {key: adams2024, topic: retail, author: "Adams Williams", title: "Zone Pricing in Retail Oligopoly", year: 2019} |
modified
paper/appendix/appendix.tex
+8 −90
@@ -1,93 +1,11 @@ | ||
| 1 | −% ============================================================================ | |
| 2 | −% Appendix | |
| 3 | −% ============================================================================ | |
| 4 | −\newpage | |
| 5 | −\begin{appendices} | |
| 1 | +\appendix | |
| 2 | +\section{Supplementary material}\label{app:supp} | |
| 6 | 3 | |
| 7 | −\renewcommand{\thetable}{A\arabic{table}} | |
| 8 | −\renewcommand{\thefigure}{A\arabic{figure}} | |
| 9 | −\setcounter{table}{0} | |
| 10 | −\setcounter{figure}{0} | |
| 4 | +\subsection{Matching rules in detail} | |
| 5 | +Cleaned name key: strip accents, lower-case, remove size tokens (\texttt{\textbackslash d+ x? \textbackslash d* (kg|g|mg|lb|oz|ml|cl|l|un|ea|pk|ch)}), punctuation, stop words (de, du, des, la, le, les, et, à, pour, the, of, and, with, un, une, sans, avec, ou, or) and the brand's own tokens; append the residual of the package label (e.g.\ ``calibre 21-25'') after removing deposit and unit-price fragments; sort tokens. Numeric tokens must be identical. Package key: total quantity in base unit rounded to 0.1 and multipack count. Tier~B: identical token sets; tier~C: rapidfuzz \texttt{token\_sort\_ratio} $\ge 0.90$; tier~D: identical full-name token sets with brand tokens present. The calibration table over scorers and thresholds, the recall analysis against UPC truth, the three validation samples (the rejected token-set version, round 1 and round 2) and the LLM prompts are in the replication package. | |
| 11 | 6 | |
| 12 | −\section{Additional Tables and Figures} | |
| 13 | −\label{app:tables} | |
| 7 | +\subsection{Preregistered hypotheses (abridged)} | |
| 8 | +H1: median within-group max--min log gap $\ge 0.05$ and AR(1) of banner-pair gaps $\ge 0.5$, placebo by label permutation. H2: ratio of raw to reference weekly volatility $\ne 1$, bootstrap over listings; falsification on single-store shops. H3: mean change of the reference price after promotion episodes $= 0$ (null), direction left to the data; event study with pretrend test, censoring and overlap rules; placebo at random dates. H4: distributed lags 0--3 with leads as placebo; geography untestable. H5: naive mean vs.\ matched-model gap $= 0$ (null), bootstrap; StatCan coherence check. H6: $S(\infty)-S(2)=0$ (null) on a 30-item basket fixed before computation. Full text, estimands, clustering and missingness rules: \texttt{research/preregistration.md} (SHA-256 in \texttt{research/decisions.md}). | |
| 14 | 9 | |
| 15 | −% --- Appendix Table: Sub-sample Analysis --- | |
| 16 | −\begin{table}[htbp] | |
| 17 | − \centering | |
| 18 | − \begin{threeparttable} | |
| 19 | − \caption{Sub-sample Analysis} | |
| 20 | − \label{tab:app_subsample} | |
| 21 | − \begin{tabular}{@{}lccc@{}} | |
| 22 | − \toprule | |
| 23 | − & (1) & (2) & (3) \\ | |
| 24 | − & Full Sample & Pre-2008 & Post-2008 \\ | |
| 25 | − \midrule | |
| 26 | − $X_{i,t}$ & 0.172$^{***}$ & 0.195$^{***}$ & 0.148$^{***}$ \\ | |
| 27 | − & (0.033) & (0.048) & (0.041) \\[6pt] | |
| 28 | − \midrule | |
| 29 | − Controls & Yes & Yes & Yes \\ | |
| 30 | − Firm FE & Yes & Yes & Yes \\ | |
| 31 | − Time FE & Yes & Yes & Yes \\[3pt] | |
| 32 | − Observations & 125{,}400 & 58{,}200 & 67{,}200 \\ | |
| 33 | − Adjusted $R^2$ & 0.190 & 0.175 & 0.203 \\ | |
| 34 | − \bottomrule | |
| 35 | − \end{tabular} | |
| 36 | − \begin{tablenotes}[flushleft] | |
| 37 | − \footnotesize | |
| 38 | − \item \textit{Notes:} This table reports sub-sample results. Column~(1) reproduces the baseline estimate from Table~\ref{tab:regression}, Column~(4). Columns~(2) and~(3) split the sample before and after January~2008. All specifications include the full set of controls and firm and time fixed effects. Standard errors clustered at the firm level are in parentheses. $^{***}$, $^{**}$, and $^{*}$ denote significance at the 1\%, 5\%, and 10\% levels. | |
| 39 | − \end{tablenotes} | |
| 40 | − \end{threeparttable} | |
| 41 | −\end{table} | |
| 42 | − | |
| 43 | −% --- Appendix Table: Robustness Checks --- | |
| 44 | −\begin{table}[htbp] | |
| 45 | − \centering | |
| 46 | − \begin{threeparttable} | |
| 47 | − \caption{Robustness Checks} | |
| 48 | − \label{tab:app_robustness} | |
| 49 | − \begin{tabular}{@{}lccc@{}} | |
| 50 | − \toprule | |
| 51 | − & (1) & (2) & (3) \\ | |
| 52 | − & Alt.\ Dep.\ Var. & Alt.\ Controls & Winsorized 5\% \\ | |
| 53 | − \midrule | |
| 54 | − $X_{i,t}$ & 0.165$^{***}$ & 0.178$^{***}$ & 0.169$^{***}$ \\ | |
| 55 | − & (0.036) & (0.034) & (0.032) \\[6pt] | |
| 56 | − \midrule | |
| 57 | − Controls & Yes & Yes & Yes \\ | |
| 58 | − Firm FE & Yes & Yes & Yes \\ | |
| 59 | − Time FE & Yes & Yes & Yes \\[3pt] | |
| 60 | − Observations & 125{,}400 & 125{,}400 & 119{,}800 \\ | |
| 61 | − Adjusted $R^2$ & 0.185 & 0.192 & 0.195 \\ | |
| 62 | − \bottomrule | |
| 63 | − \end{tabular} | |
| 64 | − \begin{tablenotes}[flushleft] | |
| 65 | − \footnotesize | |
| 66 | − \item \textit{Notes:} This table reports robustness checks. Column~(1) uses an alternative dependent variable. Column~(2) includes additional control variables. Column~(3) winsorizes all continuous variables at the 5th and 95th percentiles. All specifications include firm and time fixed effects. Standard errors clustered at the firm level are in parentheses. $^{***}$, $^{**}$, and $^{*}$ denote significance at the 1\%, 5\%, and 10\% levels. | |
| 67 | − \end{tablenotes} | |
| 68 | − \end{threeparttable} | |
| 69 | −\end{table} | |
| 70 | − | |
| 71 | −\section{Variable Definitions} | |
| 72 | −\label{app:variables} | |
| 73 | − | |
| 74 | −\begin{table}[htbp] | |
| 75 | − \centering | |
| 76 | − \caption{Variable Definitions} | |
| 77 | − \label{tab:app_variables} | |
| 78 | − \begin{tabular}{@{}p{3.5cm}p{10cm}@{}} | |
| 79 | − \toprule | |
| 80 | − Variable & Definition \\ | |
| 81 | − \midrule | |
| 82 | − $r_{i,t}$ & Monthly log return of firm $i$ in month $t$, computed as $\ln(P_{i,t}/P_{i,t-1})$. \\[4pt] | |
| 83 | − $\sigma_{i,t}$ & Monthly realized volatility of firm $i$, computed as the standard deviation of daily returns within month $t$, annualized by multiplying by $\sqrt{252}$. \\[4pt] | |
| 84 | − $X_{i,t}$ & Main explanatory variable. [Provide specific definition.] \\[4pt] | |
| 85 | − Size & Natural logarithm of market capitalization (in millions of USD) at the end of month $t-1$. \\[4pt] | |
| 86 | − Book-to-Market & Ratio of book equity to market equity, using the most recent fiscal year-end book value and the market value at the end of June. Updated annually following \citet{fama1993common}. \\[4pt] | |
| 87 | − Leverage & Total debt divided by total assets, measured at the most recent fiscal year-end. \\[4pt] | |
| 88 | − ROA & Return on assets, defined as net income divided by total assets. \\ | |
| 89 | − \bottomrule | |
| 90 | − \end{tabular} | |
| 91 | −\end{table} | |
| 92 | − | |
| 93 | −\end{appendices} | |
| 10 | +\subsection{Additional tables} | |
| 11 | +Per-feed coverage for all 54 feeds, category-level gaps with Holm-corrected sign tests, volatility ratios by banner and category, index changes by banner and category, basket item coverage, block sensitivity, the travel-cost grid, and per-origin forecast errors are provided as CSV files in \texttt{data/processed/} and as JSON summaries in \texttt{experiments/}. | |
modified
paper/figures/fig_basket.pdf
+0 −0
Binary file not shown.
modified
paper/figures/fig_coverage.pdf
+0 −0
Binary file not shown.
modified
paper/figures/fig_dispersion.pdf
+0 −0
Binary file not shown.
modified
paper/figures/fig_forecast.pdf
+0 −0
Binary file not shown.
modified
paper/figures/fig_heatmap.pdf
+0 −0
Binary file not shown.
modified
paper/figures/fig_indices.pdf
+0 −0
Binary file not shown.
added
paper/figures/fig_matching.pdf
+0 −0
Binary file not shown.
modified
paper/figures/fig_promotions.pdf
+0 −0
Binary file not shown.
modified
paper/figures/fig_qgpai.pdf
+0 −0
Binary file not shown.
modified
paper/figures/fig_spillovers.pdf
+0 −0
Binary file not shown.
modified
paper/figures/fig_store_mixture.pdf
+0 −0
Binary file not shown.
added
paper/main.pdf
+0 −0
Binary file not shown.
modified
paper/main.tex
+14 −11
@@ -42,6 +42,7 @@ | ||
| 42 | 42 | \usepackage{graphicx} |
| 43 | 43 | \usepackage{subcaption} |
| 44 | 44 | \usepackage{float} |
| 45 | +\usepackage{adjustbox} | |
| 45 | 46 | |
| 46 | 47 | % --- Colors & Links --- |
| 47 | 48 | \usepackage[dvipsnames]{xcolor} |
@@ -89,19 +90,16 @@ | ||
| 89 | 90 | % ============================================================================ |
| 90 | 91 | % METADATA — Edit these fields for each working paper |
| 91 | 92 | % ============================================================================ |
| 92 | −\newcommand{\WPnumber}{XX} | |
| 93 | −\newcommand{\WPtitle}{Your Working Paper Title Here} | |
| 94 | −\newcommand{\WPsubtitle}{} % Leave empty if no subtitle | |
| 95 | −\newcommand{\WPdate}{\today} | |
| 93 | +\newcommand{\WPnumber}{37} | |
| 94 | +\newcommand{\WPtitle}{The Hidden Architecture of Grocery Prices: Promotions, Retail Competition, and the Cost of Living in Quebec} | |
| 95 | +\newcommand{\WPsubtitle}{Evidence from 300{,}613 posted-price events across 54 Quebec grocery feeds, August--November 2026} | |
| 96 | +\newcommand{\WPdate}{November 2026} | |
| 96 | 97 | \newcommand{\WPversion}{1.0} |
| 97 | 98 | \newcommand{\WPabstract}{% |
| 98 | − This paper examines \ldots{} | |
| 99 | − We find that \ldots{} | |
| 100 | − Our results suggest \ldots{} | |
| 101 | − These findings contribute to the literature on \ldots{} | |
| 99 | +Grocery prices are not a single inflation process. Using a read-only research copy of Food-Ka, a Quebec grocery price aggregator, we assemble an auditable panel of 124{,}579 listings from 54 active source feeds (26 retail companies, two channels: online storefronts and weekly circulars) observed every seven hours between 12 August and 1 November 2026 (300{,}613 logged price events, 723{,}140 listing-weeks). We first document a measurement finding that matters for any study of scraped grocery prices: storefronts that answer anonymous sessions with store-specific prices generate intra-day oscillation among three to five price levels, so that sub-weekly ``volatility'' in such feeds is a mixture over unidentified stores, not repricing. We then build a calibrated cross-banner matching procedure (exact UPC within Metro Inc.; brand, package and token-identical names elsewhere), validated against 4{,}413 UPC-identified pairs (precision 0.91, recall 0.98) and a 240-pair reviewed sample, which yields 5{,}909 cross-banner groups. On identical products in the same week, the median max--min log price gap across banners is 0.154 (79\% of group-weeks exceed 5\%), banner-pair gaps are persistent (AR(1) 0.66 against a 0.00 permutation placebo), and banner fixed effects range from $-0.20$ (Walmart, Giant Tiger, Maxi) to $+0.12$ (specialty shops) relative to Metro. Inferred promotions cover 4.6\% of storefront listing-weeks with a median depth of 22\%; half of all weekly price changes are promotion entries or exits, doubling measured volatility (ratio 1.92, CI 1.84--2.02). Temporary sales return exactly to their pre-sale reference (mean change $-0.0003$ log points), i.e. we find no reference-price drift after promotions in this window; a pre-sale price elevation is concentrated in store-mixture feeds and shrinks to about two log points in single-store shops. Lead--lag price associations between rivals are small and fail a lead placebo for cross-company pairs, so no causal response is claimed. Over eleven weeks, matched-model and naive indices agree within one percentage point for storefronts, but naive averages are off by 10--30 points for banners with assortment turnover. A fixed-basket mixed-integer program shows that unconstrained cheapest-banner shopping saves 15\% relative to the best single banner, 85\% of which is captured with two banners; store geography and travel costs are unobserved and treated as explicit limitations. We publish the five-component Quebec Grocery Price and Affordability Index with uncertainty bands and a prospective collection plan, and we report null and failed tests alongside positive ones.% | |
| 102 | 100 | } |
| 103 | −\newcommand{\WPkeywords}{Keyword 1, Keyword 2, Keyword 3, Keyword 4} | |
| 104 | −\newcommand{\WPjel}{G12, G14, C58} | |
| 101 | +\newcommand{\WPkeywords}{grocery prices; price dispersion; promotions; high--low pricing; web-scraped prices; price indices; record linkage; retail competition; Quebec} | |
| 102 | +\newcommand{\WPjel}{D22, D40, E31, L11, L81, C81} | |
| 105 | 103 | |
| 106 | 104 | % --- Author --- |
| 107 | 105 | \newcommand{\WPauthor}{Simon-Pierre Boucher} |
@@ -127,11 +125,16 @@ | ||
| 127 | 125 | % --- Main Body --- |
| 128 | 126 | \input{sections/introduction} |
| 129 | 127 | \input{sections/literature} |
| 128 | +\input{sections/institutional} | |
| 130 | 129 | \input{sections/data} |
| 131 | −\input{sections/methodology} | |
| 130 | +\input{sections/theory} | |
| 131 | +\input{sections/methods} | |
| 132 | 132 | \input{sections/results} |
| 133 | 133 | \input{sections/robustness} |
| 134 | +\input{sections/policy} | |
| 135 | +\input{sections/limitations} | |
| 134 | 136 | \input{sections/conclusion} |
| 137 | +\input{sections/reproducibility} | |
| 135 | 138 | |
| 136 | 139 | % --- References --- |
| 137 | 140 | \newpage |
modified
paper/references.bib
+774 −54
@@ -1,61 +1,781 @@ | ||
| 1 | −% ============================================================================ | |
| 2 | −% References — BibTeX Database | |
| 3 | −% ============================================================================ | |
| 4 | − | |
| 5 | −@article{fama1993common, | |
| 6 | − author = {Fama, Eugene F. and French, Kenneth R.}, | |
| 7 | − title = {Common Risk Factors in the Returns on Stocks and Bonds}, | |
| 8 | − journal = {Journal of Financial Economics}, | |
| 9 | − volume = {33}, | |
| 10 | − number = {1}, | |
| 11 | − pages = {3--56}, | |
| 12 | − year = {1993} | |
| 13 | −} | |
| 14 | − | |
| 15 | −@article{bollerslev1986generalized, | |
| 16 | − author = {Bollerslev, Tim}, | |
| 17 | − title = {Generalized Autoregressive Conditional Heteroskedasticity}, | |
| 18 | − journal = {Journal of Econometrics}, | |
| 19 | − volume = {31}, | |
| 20 | − number = {3}, | |
| 21 | − pages = {307--327}, | |
| 22 | − year = {1986} | |
| 1 | +% Generated from literature/references_verified.csv — every entry verified via Crossref or a stable URL | |
| 2 | +@article{stigler1961, | |
| 3 | + author = {Stigler, George J.}, | |
| 4 | + title = {The Economics of Information}, | |
| 5 | + journal = {Journal of Political Economy}, | |
| 6 | + year = {1961}, | |
| 7 | + volume = {69}, | |
| 8 | + number = {3}, | |
| 9 | + pages = {213--225}, | |
| 10 | + doi = {10.1086/258464} | |
| 11 | +} | |
| 12 | + | |
| 13 | +@article{salop1977, | |
| 14 | + author = {Salop, Steven and Stiglitz, Joseph}, | |
| 15 | + title = {Bargains and Ripoffs: A Model of Monopolistically Competitive Price Dispersion}, | |
| 16 | + journal = {The Review of Economic Studies}, | |
| 17 | + year = {1977}, | |
| 18 | + volume = {44}, | |
| 19 | + number = {3}, | |
| 20 | + pages = {493}, | |
| 21 | + doi = {10.2307/2296903} | |
| 23 | 22 | } |
| 24 | 23 | |
| 25 | −@article{newey1987simple, | |
| 26 | − author = {Newey, Whitney K. and West, Kenneth D.}, | |
| 27 | − title = {A Simple, Positive Semi-Definite, Heteroskedasticity and Autocorrelation Consistent Covariance Matrix}, | |
| 24 | +@article{burdett1983, | |
| 25 | + author = {Burdett, Kenneth and Judd, Kenneth L.}, | |
| 26 | + title = {Equilibrium Price Dispersion}, | |
| 28 | 27 | journal = {Econometrica}, |
| 29 | − volume = {55}, | |
| 30 | − number = {3}, | |
| 31 | − pages = {703--708}, | |
| 32 | − year = {1987} | |
| 28 | + year = {1983}, | |
| 29 | + volume = {51}, | |
| 30 | + number = {4}, | |
| 31 | + pages = {955}, | |
| 32 | + doi = {10.2307/1912045} | |
| 33 | +} | |
| 34 | + | |
| 35 | +@article{sorensen2000, | |
| 36 | + author = {Sorensen, Alan T.}, | |
| 37 | + title = {Equilibrium Price Dispersion in Retail Markets for Prescription Drugs}, | |
| 38 | + journal = {Journal of Political Economy}, | |
| 39 | + year = {2000}, | |
| 40 | + volume = {108}, | |
| 41 | + number = {4}, | |
| 42 | + pages = {833--850}, | |
| 43 | + doi = {10.1086/316103} | |
| 44 | +} | |
| 45 | + | |
| 46 | +@techreport{lach2002, | |
| 47 | + author = {Lach, Saul}, | |
| 48 | + title = {Existence and Persistence of Price Dispersion: an Empirical Analysis}, | |
| 49 | + institution = {Competition Bureau Canada}, | |
| 50 | + year = {2002}, | |
| 51 | + url = {https://competition-bureau.canada.ca} | |
| 52 | +} | |
| 53 | + | |
| 54 | +@article{baye2006, | |
| 55 | + author = {Baye, Michael R. and Morgan, John and Scholten, Patrick}, | |
| 56 | + title = {Chapter 6 Information, Search, and Price Dispersion}, | |
| 57 | + journal = {Handbooks in Information Systems Economics and Information Systems}, | |
| 58 | + year = {2006}, | |
| 59 | + pages = {323--375}, | |
| 60 | + doi = {10.1016/s1574-0145(06)01006-3} | |
| 61 | +} | |
| 62 | + | |
| 63 | +@techreport{kaplan2015, | |
| 64 | + author = {Kaplan, Greg and Menzio, Guido}, | |
| 65 | + title = {The Morphology of Price Dispersion}, | |
| 66 | + institution = {Competition Bureau Canada}, | |
| 67 | + year = {2014}, | |
| 68 | + url = {https://competition-bureau.canada.ca} | |
| 69 | +} | |
| 70 | + | |
| 71 | +@article{kaplan2019, | |
| 72 | + author = {Kaplan, Greg and Menzio, Guido and Rudanko, Leena and Trachter, Nicholas}, | |
| 73 | + title = {Relative Price Dispersion: Evidence and Theory}, | |
| 74 | + journal = {American Economic Journal: Microeconomics}, | |
| 75 | + year = {2019}, | |
| 76 | + volume = {11}, | |
| 77 | + number = {3}, | |
| 78 | + pages = {68--124}, | |
| 79 | + doi = {10.1257/mic.20170126} | |
| 80 | +} | |
| 81 | + | |
| 82 | +@article{hitsch2021, | |
| 83 | + author = {Hitsch, Günter J. and Hortaçsu, Ali and Lin, Xiliang}, | |
| 84 | + title = {Prices and promotions in U.S. retail markets}, | |
| 85 | + journal = {Quantitative Marketing and Economics}, | |
| 86 | + year = {2021}, | |
| 87 | + volume = {19}, | |
| 88 | + number = {3-4}, | |
| 89 | + pages = {289--368}, | |
| 90 | + doi = {10.1007/s11129-021-09238-x} | |
| 91 | +} | |
| 92 | + | |
| 93 | +@article{pesendorfer2002, | |
| 94 | + author = {Pesendorfer, Martin}, | |
| 95 | + title = {Retail Sales: A Study of Pricing Behavior in Supermarkets}, | |
| 96 | + journal = {The Journal of Business}, | |
| 97 | + year = {2002}, | |
| 98 | + volume = {75}, | |
| 99 | + number = {1}, | |
| 100 | + pages = {33--66}, | |
| 101 | + doi = {10.1086/323504} | |
| 102 | +} | |
| 103 | + | |
| 104 | +@techreport{hendel2006, | |
| 105 | + author = {Hendel, Igal and Nevo, Aviv}, | |
| 106 | + title = {Measuring the Implications of Sales and Consumer Inventory Behavior}, | |
| 107 | + institution = {Competition Bureau Canada}, | |
| 108 | + year = {2005}, | |
| 109 | + url = {https://competition-bureau.canada.ca} | |
| 110 | +} | |
| 111 | + | |
| 112 | +@article{hendel2013, | |
| 113 | + author = {Hendel, Igal and Nevo, Aviv}, | |
| 114 | + title = {Intertemporal Price Discrimination in Storable Goods Markets}, | |
| 115 | + journal = {American Economic Review}, | |
| 116 | + year = {2013}, | |
| 117 | + volume = {103}, | |
| 118 | + number = {7}, | |
| 119 | + pages = {2722--2751}, | |
| 120 | + doi = {10.1257/aer.103.7.2722} | |
| 121 | +} | |
| 122 | + | |
| 123 | +@article{hosken2004, | |
| 124 | + author = {Hosken, Daniel and Reiffen, David}, | |
| 125 | + title = {Patterns of Retail Price Variation}, | |
| 126 | + journal = {The RAND Journal of Economics}, | |
| 127 | + year = {2004}, | |
| 128 | + volume = {35}, | |
| 129 | + number = {1}, | |
| 130 | + pages = {128}, | |
| 131 | + doi = {10.2307/1593733} | |
| 132 | +} | |
| 133 | + | |
| 134 | +@article{nakamura2008, | |
| 135 | + author = {Nakamura, Emi and Steinsson, Jón}, | |
| 136 | + title = {Five Facts about Prices: A Reevaluation of Menu Cost Models | |
| 137 | + <sup>*</sup>}, | |
| 138 | + journal = {Quarterly Journal of Economics}, | |
| 139 | + year = {2008}, | |
| 140 | + volume = {123}, | |
| 141 | + number = {4}, | |
| 142 | + pages = {1415--1464}, | |
| 143 | + doi = {10.1162/qjec.2008.123.4.1415} | |
| 144 | +} | |
| 145 | + | |
| 146 | +@article{klenow2008, | |
| 147 | + author = {Klenow, Peter J. and Kryvtsov, Oleksiy}, | |
| 148 | + title = {State-Dependent or Time-Dependent Pricing: Does It Matter for Recent U.S. Inflation? | |
| 149 | + <sup>*</sup>}, | |
| 150 | + journal = {Quarterly Journal of Economics}, | |
| 151 | + year = {2008}, | |
| 152 | + volume = {123}, | |
| 153 | + number = {3}, | |
| 154 | + pages = {863--904}, | |
| 155 | + doi = {10.1162/qjec.2008.123.3.863} | |
| 156 | +} | |
| 157 | + | |
| 158 | +@article{eichenbaum2011, | |
| 159 | + author = {Eichenbaum, Martin and Jaimovich, Nir and Rebelo, Sergio}, | |
| 160 | + title = {Reference Prices, Costs, and Nominal Rigidities}, | |
| 161 | + journal = {American Economic Review}, | |
| 162 | + year = {2011}, | |
| 163 | + volume = {101}, | |
| 164 | + number = {1}, | |
| 165 | + pages = {234--262}, | |
| 166 | + doi = {10.1257/aer.101.1.234} | |
| 167 | +} | |
| 168 | + | |
| 169 | +@article{kehoe2015, | |
| 170 | + author = {Kehoe, Patrick and Midrigan, Virgiliu}, | |
| 171 | + title = {Prices are sticky after all}, | |
| 172 | + journal = {Journal of Monetary Economics}, | |
| 173 | + year = {2015}, | |
| 174 | + volume = {75}, | |
| 175 | + pages = {35--53}, | |
| 176 | + doi = {10.1016/j.jmoneco.2014.12.004} | |
| 177 | +} | |
| 178 | + | |
| 179 | +@article{anderson2017, | |
| 180 | + author = {Anderson, Eric and Malin, Benjamin A. and Nakamura, Emi and Simester, Duncan and Steinsson, Jón}, | |
| 181 | + title = {Informational rigidities and the stickiness of temporary Sales}, | |
| 182 | + journal = {Journal of Monetary Economics}, | |
| 183 | + year = {2017}, | |
| 184 | + volume = {90}, | |
| 185 | + pages = {64--83}, | |
| 186 | + doi = {10.1016/j.jmoneco.2017.06.003} | |
| 187 | +} | |
| 188 | + | |
| 189 | +@article{chevalier2019, | |
| 190 | + author = {Chevalier, Judith A. and Kashyap, Anil K}, | |
| 191 | + title = {Best Prices: Price Discrimination and Consumer Substitution}, | |
| 192 | + journal = {American Economic Journal: Economic Policy}, | |
| 193 | + year = {2019}, | |
| 194 | + volume = {11}, | |
| 195 | + number = {1}, | |
| 196 | + pages = {126--159}, | |
| 197 | + doi = {10.1257/pol.20150362} | |
| 198 | +} | |
| 199 | + | |
| 200 | +@article{coibion2015, | |
| 201 | + author = {Coibion, Olivier and Gorodnichenko, Yuriy and Hong, Gee Hee}, | |
| 202 | + title = {The Cyclicality of Sales, Regular and Effective Prices: Business Cycle and Policy Implications}, | |
| 203 | + journal = {American Economic Review}, | |
| 204 | + year = {2015}, | |
| 205 | + volume = {105}, | |
| 206 | + number = {3}, | |
| 207 | + pages = {993--1029}, | |
| 208 | + doi = {10.1257/aer.20121546} | |
| 33 | 209 | } |
| 34 | 210 | |
| 35 | −@article{white1980heteroskedasticity, | |
| 36 | − author = {White, Halbert}, | |
| 37 | − title = {A Heteroskedasticity-Consistent Covariance Matrix Estimator and a Direct Test for Heteroskedasticity}, | |
| 211 | +@article{golosov2007, | |
| 212 | + author = {Golosov, Mikhail and Lucas Jr., Robert E.}, | |
| 213 | + title = {Menu Costs and Phillips Curves}, | |
| 214 | + journal = {Journal of Political Economy}, | |
| 215 | + year = {2007}, | |
| 216 | + volume = {115}, | |
| 217 | + number = {2}, | |
| 218 | + pages = {171--199}, | |
| 219 | + doi = {10.1086/512625} | |
| 220 | +} | |
| 221 | + | |
| 222 | +@techreport{nakamura2013, | |
| 223 | + author = {Nakamura, Emi and Steinsson, Jón}, | |
| 224 | + title = {Price Rigidity: Microeconomic Evidence and Macroeconomic Implications}, | |
| 225 | + institution = {Competition Bureau Canada}, | |
| 226 | + year = {2013}, | |
| 227 | + url = {https://competition-bureau.canada.ca} | |
| 228 | +} | |
| 229 | + | |
| 230 | +@article{ivancic2011, | |
| 231 | + author = {Ivancic, Lorraine and Erwin Diewert, W. and Fox, Kevin J.}, | |
| 232 | + title = {Scanner data, time aggregation and the construction of price indexes}, | |
| 233 | + journal = {Journal of Econometrics}, | |
| 234 | + year = {2011}, | |
| 235 | + volume = {161}, | |
| 236 | + number = {1}, | |
| 237 | + pages = {24--35}, | |
| 238 | + doi = {10.1016/j.jeconom.2010.09.003} | |
| 239 | +} | |
| 240 | + | |
| 241 | +@article{dehaan2011, | |
| 242 | + author = {de Haan, Jan and van der Grient, Heymerik A.}, | |
| 243 | + title = {Eliminating chain drift in price indexes based on scanner data}, | |
| 244 | + journal = {Journal of Econometrics}, | |
| 245 | + year = {2011}, | |
| 246 | + volume = {161}, | |
| 247 | + number = {1}, | |
| 248 | + pages = {36--46}, | |
| 249 | + doi = {10.1016/j.jeconom.2010.09.004} | |
| 250 | +} | |
| 251 | + | |
| 252 | +@article{redding2020, | |
| 253 | + author = {Redding, Stephen J and Weinstein, David E}, | |
| 254 | + title = {Measuring Aggregate Price Indices with Taste Shocks: Theory and Evidence for CES Preferences*}, | |
| 255 | + journal = {The Quarterly Journal of Economics}, | |
| 256 | + year = {2019}, | |
| 257 | + volume = {135}, | |
| 258 | + number = {1}, | |
| 259 | + pages = {503--560}, | |
| 260 | + doi = {10.1093/qje/qjz031} | |
| 261 | +} | |
| 262 | + | |
| 263 | +@techreport{cavallo2017, | |
| 264 | + author = {Cavallo, Alberto}, | |
| 265 | + title = {Are Online and Offline Prices Similar? Evidence from Large Multi-Channel Retailers}, | |
| 266 | + institution = {Competition Bureau Canada}, | |
| 267 | + year = {2016}, | |
| 268 | + url = {https://competition-bureau.canada.ca} | |
| 269 | +} | |
| 270 | + | |
| 271 | +@techreport{cavallo2016, | |
| 272 | + author = {Cavallo, Alberto and Rigobon, Roberto}, | |
| 273 | + title = {The Billion Prices Project: Using Online Prices for Measurement and Research}, | |
| 274 | + institution = {Competition Bureau Canada}, | |
| 275 | + year = {2016}, | |
| 276 | + url = {https://competition-bureau.canada.ca} | |
| 277 | +} | |
| 278 | + | |
| 279 | +@article{cavallo2018, | |
| 280 | + author = {Cavallo, Alberto}, | |
| 281 | + title = {Scraped Data and Sticky Prices}, | |
| 282 | + journal = {The Review of Economics and Statistics}, | |
| 283 | + year = {2018}, | |
| 284 | + volume = {100}, | |
| 285 | + number = {1}, | |
| 286 | + pages = {105--119}, | |
| 287 | + doi = {10.1162/rest_a_00652} | |
| 288 | +} | |
| 289 | + | |
| 290 | +@article{gorodnichenko2017, | |
| 291 | + author = {Gorodnichenko, Yuriy and Talavera, Oleksandr}, | |
| 292 | + title = {Price Setting in Online Markets: Basic Facts, International Comparisons, and Cross-Border Integration}, | |
| 293 | + journal = {American Economic Review}, | |
| 294 | + year = {2017}, | |
| 295 | + volume = {107}, | |
| 296 | + number = {1}, | |
| 297 | + pages = {249--282}, | |
| 298 | + doi = {10.1257/aer.20141127} | |
| 299 | +} | |
| 300 | + | |
| 301 | +@article{ellickson2008, | |
| 302 | + author = {Ellickson, Paul B. and Misra, Sanjog}, | |
| 303 | + title = {Supermarket Pricing Strategies}, | |
| 304 | + journal = {Marketing Science}, | |
| 305 | + year = {2008}, | |
| 306 | + volume = {27}, | |
| 307 | + number = {5}, | |
| 308 | + pages = {811--828}, | |
| 309 | + doi = {10.1287/mksc.1080.0398} | |
| 310 | +} | |
| 311 | + | |
| 312 | +@techreport{hausman2007, | |
| 313 | + author = {Hausman, Jerry and Leibtag, Ephraim}, | |
| 314 | + title = {Consumer Benefits from Increased Competition in Shopping Outlets: Measuring the Effect of Wal-Mart}, | |
| 315 | + institution = {Competition Bureau Canada}, | |
| 316 | + year = {2006}, | |
| 317 | + url = {https://competition-bureau.canada.ca} | |
| 318 | +} | |
| 319 | + | |
| 320 | +@article{lal1997, | |
| 321 | + author = {Lal, Rajiv and Rao, Ram}, | |
| 322 | + title = {Supermarket Competition: The Case of Every Day Low Pricing}, | |
| 323 | + journal = {Marketing Science}, | |
| 324 | + year = {1997}, | |
| 325 | + volume = {16}, | |
| 326 | + number = {1}, | |
| 327 | + pages = {60--80}, | |
| 328 | + doi = {10.1287/mksc.16.1.60} | |
| 329 | +} | |
| 330 | + | |
| 331 | +@article{hoch1994, | |
| 332 | + author = {Hoch, Stephen J. and Drèze, Xavier and Purk, Mary E.}, | |
| 333 | + title = {EDLP, Hi-Lo, and Margin Arithmetic}, | |
| 334 | + journal = {Journal of Marketing}, | |
| 335 | + year = {1994}, | |
| 336 | + volume = {58}, | |
| 337 | + number = {4}, | |
| 338 | + pages = {16--27}, | |
| 339 | + doi = {10.1177/002224299405800402} | |
| 340 | +} | |
| 341 | + | |
| 342 | +@article{aguiar2007, | |
| 343 | + author = {Aguiar, Mark and Hurst, Erik}, | |
| 344 | + title = {Life-Cycle Prices and Production}, | |
| 345 | + journal = {American Economic Review}, | |
| 346 | + year = {2007}, | |
| 347 | + volume = {97}, | |
| 348 | + number = {5}, | |
| 349 | + pages = {1533--1559}, | |
| 350 | + doi = {10.1257/aer.97.5.1533} | |
| 351 | +} | |
| 352 | + | |
| 353 | +@article{kaplan2016, | |
| 354 | + author = {Kaplan, Greg and Menzio, Guido}, | |
| 355 | + title = {Shopping Externalities and Self-Fulfilling Unemployment Fluctuations}, | |
| 356 | + journal = {Journal of Political Economy}, | |
| 357 | + year = {2016}, | |
| 358 | + volume = {124}, | |
| 359 | + number = {3}, | |
| 360 | + pages = {771--825}, | |
| 361 | + doi = {10.1086/685909} | |
| 362 | +} | |
| 363 | + | |
| 364 | +@article{griffith2009, | |
| 365 | + author = {Griffith, Rachel and Leibtag, Ephraim and Leicester, Andrew and Nevo, Aviv}, | |
| 366 | + title = {Consumer Shopping Behavior: How Much Do Consumers Save?}, | |
| 367 | + journal = {Journal of Economic Perspectives}, | |
| 368 | + year = {2009}, | |
| 369 | + volume = {23}, | |
| 370 | + number = {2}, | |
| 371 | + pages = {99--120}, | |
| 372 | + doi = {10.1257/jep.23.2.99} | |
| 373 | +} | |
| 374 | + | |
| 375 | +@article{nevo2019, | |
| 376 | + author = {Nevo, Aviv and Wong, Arlene}, | |
| 377 | + title = {THE ELASTICITY OF SUBSTITUTION BETWEEN TIME AND MARKET GOODS: EVIDENCE FROM THE GREAT RECESSION}, | |
| 378 | + journal = {International Economic Review}, | |
| 379 | + year = {2018}, | |
| 380 | + volume = {60}, | |
| 381 | + number = {1}, | |
| 382 | + pages = {25--51}, | |
| 383 | + doi = {10.1111/iere.12343} | |
| 384 | +} | |
| 385 | + | |
| 386 | +@article{jaravel2019, | |
| 387 | + author = {Jaravel, Xavier}, | |
| 388 | + title = {The Unequal Gains from Product Innovations: Evidence from the U.S. Retail Sector*}, | |
| 389 | + journal = {The Quarterly Journal of Economics}, | |
| 390 | + year = {2018}, | |
| 391 | + volume = {134}, | |
| 392 | + number = {2}, | |
| 393 | + pages = {715--783}, | |
| 394 | + doi = {10.1093/qje/qjy031} | |
| 395 | +} | |
| 396 | + | |
| 397 | +@article{argente2021, | |
| 398 | + author = {Argente, David and Lee, Munseob}, | |
| 399 | + title = {Cost of Living Inequality During the Great Recession}, | |
| 400 | + journal = {Journal of the European Economic Association}, | |
| 401 | + year = {2020}, | |
| 402 | + volume = {19}, | |
| 403 | + number = {2}, | |
| 404 | + pages = {913--952}, | |
| 405 | + doi = {10.1093/jeea/jvaa018} | |
| 406 | +} | |
| 407 | + | |
| 408 | +@article{dube2018, | |
| 409 | + author = {Dubé, Jean-Pierre and Hitsch, Günter J. and Rossi, Peter E.}, | |
| 410 | + title = {Income and Wealth Effects on Private-Label Demand: Evidence from the Great Recession}, | |
| 411 | + journal = {Marketing Science}, | |
| 412 | + year = {2018}, | |
| 413 | + volume = {37}, | |
| 414 | + number = {1}, | |
| 415 | + pages = {22--53}, | |
| 416 | + doi = {10.1287/mksc.2017.1047} | |
| 417 | +} | |
| 418 | + | |
| 419 | +@techreport{bronnenberg2015, | |
| 420 | + author = {Bronnenberg, Bart and Dubé, Jean-Pierre and Gentzkow, Matthew and Shapiro, Jesse}, | |
| 421 | + title = {Do Pharmacists Buy Bayer? Informed Shoppers and the Brand Premium}, | |
| 422 | + institution = {Competition Bureau Canada}, | |
| 423 | + year = {2014}, | |
| 424 | + url = {https://competition-bureau.canada.ca} | |
| 425 | +} | |
| 426 | + | |
| 427 | +@article{allcott2019, | |
| 428 | + author = {Allcott, Hunt and Diamond, Rebecca and Dubé, Jean-Pierre and Handbury, Jessie and Rahkovsky, Ilya and Schnell, Molly}, | |
| 429 | + title = {Food Deserts and the Causes of Nutritional Inequality*}, | |
| 430 | + journal = {The Quarterly Journal of Economics}, | |
| 431 | + year = {2019}, | |
| 432 | + volume = {134}, | |
| 433 | + number = {4}, | |
| 434 | + pages = {1793--1844}, | |
| 435 | + doi = {10.1093/qje/qjz015} | |
| 436 | +} | |
| 437 | + | |
| 438 | +@article{handbury2015, | |
| 439 | + author = {Handbury, J. and Weinstein, D. E.}, | |
| 440 | + title = {Goods Prices and Availability in Cities}, | |
| 441 | + journal = {The Review of Economic Studies}, | |
| 442 | + year = {2014}, | |
| 443 | + volume = {82}, | |
| 444 | + number = {1}, | |
| 445 | + pages = {258--296}, | |
| 446 | + doi = {10.1093/restud/rdu033} | |
| 447 | +} | |
| 448 | + | |
| 449 | +@article{gagnon2020, | |
| 450 | + author = {Gagnon, Etienne and López-Salido, David}, | |
| 451 | + title = {Small Price Responses to Large Demand Shocks}, | |
| 452 | + journal = {Journal of the European Economic Association}, | |
| 453 | + year = {2019}, | |
| 454 | + volume = {18}, | |
| 455 | + number = {2}, | |
| 456 | + pages = {792--828}, | |
| 457 | + doi = {10.1093/jeea/jvz002} | |
| 458 | +} | |
| 459 | + | |
| 460 | +@article{dellavigna2019, | |
| 461 | + author = {DellaVigna, Stefano and Gentzkow, Matthew}, | |
| 462 | + title = {Uniform Pricing in U.S. Retail Chains*}, | |
| 463 | + journal = {The Quarterly Journal of Economics}, | |
| 464 | + year = {2019}, | |
| 465 | + volume = {134}, | |
| 466 | + number = {4}, | |
| 467 | + pages = {2011--2084}, | |
| 468 | + doi = {10.1093/qje/qjz019} | |
| 469 | +} | |
| 470 | + | |
| 471 | +@techreport{hitsch2019, | |
| 472 | + author = {Hitsch, Günter and Hortaçsu, Ali and Lin, Xiliang}, | |
| 473 | + title = {Prices and Promotions in U.S. Retail Markets: Evidence from Big Data}, | |
| 474 | + institution = {Competition Bureau Canada}, | |
| 475 | + year = {2019}, | |
| 476 | + url = {https://competition-bureau.canada.ca} | |
| 477 | +} | |
| 478 | + | |
| 479 | +@article{melser2018, | |
| 480 | + author = {Melser, Daniel}, | |
| 481 | + title = {Scanner Data Price Indexes: Addressing Some Unresolved Issues}, | |
| 482 | + journal = {Journal of Business \& Economic Statistics}, | |
| 483 | + year = {2017}, | |
| 484 | + volume = {36}, | |
| 485 | + number = {3}, | |
| 486 | + pages = {516--522}, | |
| 487 | + doi = {10.1080/07350015.2016.1218339} | |
| 488 | +} | |
| 489 | + | |
| 490 | +@article{nakamura2018, | |
| 491 | + author = {Nakamura, Emi and Steinsson, Jón and Sun, Patrick and Villar, Daniel}, | |
| 492 | + title = {The Elusive Costs of Inflation: Price Dispersion during the U.S. Great Inflation*}, | |
| 493 | + journal = {The Quarterly Journal of Economics}, | |
| 494 | + year = {2018}, | |
| 495 | + volume = {133}, | |
| 496 | + number = {4}, | |
| 497 | + pages = {1933--1980}, | |
| 498 | + doi = {10.1093/qje/qjy017} | |
| 499 | +} | |
| 500 | + | |
| 501 | +@article{anderson2015, | |
| 502 | + author = {Anderson, Eric and Jaimovich, Nir and Simester, Duncan}, | |
| 503 | + title = {Price Stickiness: Empirical Evidence of the Menu Cost Channel}, | |
| 504 | + journal = {Review of Economics and Statistics}, | |
| 505 | + year = {2015}, | |
| 506 | + volume = {97}, | |
| 507 | + number = {4}, | |
| 508 | + pages = {813--826}, | |
| 509 | + doi = {10.1162/rest_a_00507} | |
| 510 | +} | |
| 511 | + | |
| 512 | +@article{fellegi1969, | |
| 513 | + author = {Fellegi, Ivan P. and Sunter, Alan B.}, | |
| 514 | + title = {A Theory for Record Linkage}, | |
| 515 | + journal = {Journal of the American Statistical Association}, | |
| 516 | + year = {1969}, | |
| 517 | + volume = {64}, | |
| 518 | + number = {328}, | |
| 519 | + pages = {1183--1210}, | |
| 520 | + doi = {10.1080/01621459.1969.10501049} | |
| 521 | +} | |
| 522 | + | |
| 523 | +@book{christen2012, | |
| 524 | + author = {Christen, Peter}, | |
| 525 | + title = {Data Matching}, | |
| 526 | + publisher = {Springer Berlin Heidelberg}, | |
| 527 | + year = {2012}, | |
| 528 | + doi = {10.1007/978-3-642-31164-2} | |
| 529 | +} | |
| 530 | + | |
| 531 | +@article{macdonald2000, | |
| 532 | + author = {MacDonald, James M.}, | |
| 533 | + title = {Demand, Information, and Competition: Why Do Food Prices Fall at Seasonal Demand Peaks?}, | |
| 534 | + journal = {The Journal of Industrial Economics}, | |
| 535 | + year = {2000}, | |
| 536 | + volume = {48}, | |
| 537 | + number = {1}, | |
| 538 | + pages = {27--45}, | |
| 539 | + doi = {10.1111/1467-6451.00111} | |
| 540 | +} | |
| 541 | + | |
| 542 | +@article{bils2004, | |
| 543 | + author = {Bils, Mark and Klenow, Peter J.}, | |
| 544 | + title = {Some Evidence on the Importance of Sticky Prices}, | |
| 545 | + journal = {Journal of Political Economy}, | |
| 546 | + year = {2004}, | |
| 547 | + volume = {112}, | |
| 548 | + number = {5}, | |
| 549 | + pages = {947--985}, | |
| 550 | + doi = {10.1086/422559} | |
| 551 | +} | |
| 552 | + | |
| 553 | +@article{guimaraes2011, | |
| 554 | + author = {Guimaraes, Bernardo and Sheedy, Kevin D}, | |
| 555 | + title = {Sales and Monetary Policy}, | |
| 556 | + journal = {American Economic Review}, | |
| 557 | + year = {2011}, | |
| 558 | + volume = {101}, | |
| 559 | + number = {2}, | |
| 560 | + pages = {844--876}, | |
| 561 | + doi = {10.1257/aer.101.2.844} | |
| 562 | +} | |
| 563 | + | |
| 564 | +@article{aguirregabiria1999, | |
| 565 | + author = {Aguirregabiria, Victor}, | |
| 566 | + title = {The Dynamics of Markups and Inventories in Retailing Firms}, | |
| 567 | + journal = {Review of Economic Studies}, | |
| 568 | + year = {1999}, | |
| 569 | + volume = {66}, | |
| 570 | + number = {2}, | |
| 571 | + pages = {275--308}, | |
| 572 | + doi = {10.1111/1467-937x.00088} | |
| 573 | +} | |
| 574 | + | |
| 575 | +@unpublished{gourville2004, | |
| 576 | + author = {Gourville, John T. and Koehler, Jonathan J.}, | |
| 577 | + title = {Downsizing Price Increases: A Greater Sensitivity to Price than Quantity in Consumer Markets}, | |
| 578 | + note = {Working paper, SSRN}, | |
| 579 | + year = {2004}, | |
| 580 | + doi = {10.2139/ssrn.559482} | |
| 581 | +} | |
| 582 | + | |
| 583 | +@article{cakir2014, | |
| 584 | + author = {Çakır, Metin and Balagtas, Joseph V.}, | |
| 585 | + title = {Consumer Response to Package Downsizing: Evidence from the Chicago Ice Cream Market}, | |
| 586 | + journal = {Journal of Retailing}, | |
| 587 | + year = {2014}, | |
| 588 | + volume = {90}, | |
| 589 | + number = {1}, | |
| 590 | + pages = {1--12}, | |
| 591 | + doi = {10.1016/j.jretai.2013.06.002} | |
| 592 | +} | |
| 593 | + | |
| 594 | +@article{einav2010, | |
| 595 | + author = {Einav, Liran and Leibtag, Ephraim and Nevo, Aviv}, | |
| 596 | + title = {Recording discrepancies in Nielsen Homescan data: Are they present and do they matter?}, | |
| 597 | + journal = {Quantitative Marketing and Economics}, | |
| 598 | + year = {2009}, | |
| 599 | + volume = {8}, | |
| 600 | + number = {2}, | |
| 601 | + pages = {207--239}, | |
| 602 | + doi = {10.1007/s11129-009-9073-0} | |
| 603 | +} | |
| 604 | + | |
| 605 | +@techreport{diewert2020, | |
| 606 | + author = {Diewert, W. Erwin and Fox, Kevin}, | |
| 607 | + title = {Measuring Real Consumption and CPI Bias under Lockdown Conditions}, | |
| 608 | + institution = {Competition Bureau Canada}, | |
| 609 | + year = {2020}, | |
| 610 | + url = {https://competition-bureau.canada.ca} | |
| 611 | +} | |
| 612 | + | |
| 613 | +@article{beck2024, | |
| 614 | + author = {Beck, Günter W. and Jaravel, Xavier}, | |
| 615 | + title = {Prices and Global Inequality: New Evidence from Worldwide Scanner Data}, | |
| 616 | + journal = {SSRN Electronic Journal}, | |
| 617 | + year = {2020}, | |
| 618 | + doi = {10.2139/ssrn.3671980} | |
| 619 | +} | |
| 620 | + | |
| 621 | +@article{atkin2018, | |
| 622 | + author = {Atkin, David and Faber, Benjamin and Gonzalez-Navarro, Marco}, | |
| 623 | + title = {Retail Globalization and Household Welfare: Evidence from Mexico}, | |
| 624 | + journal = {Journal of Political Economy}, | |
| 625 | + year = {2018}, | |
| 626 | + volume = {126}, | |
| 627 | + number = {1}, | |
| 628 | + pages = {1--73}, | |
| 629 | + doi = {10.1086/695476} | |
| 630 | +} | |
| 631 | + | |
| 632 | +@article{cavallo2023, | |
| 633 | + author = {Cavallo, Alberto and Kryvtsov, Oleksiy}, | |
| 634 | + title = {What can stockouts tell us about inflation? Evidence from online micro data}, | |
| 635 | + journal = {Journal of International Economics}, | |
| 636 | + year = {2023}, | |
| 637 | + volume = {146}, | |
| 638 | + pages = {103769}, | |
| 639 | + doi = {10.1016/j.jinteco.2023.103769} | |
| 640 | +} | |
| 641 | + | |
| 642 | +@article{adams2024, | |
| 643 | + author = {Adams, Brian and Williams, Kevin R.}, | |
| 644 | + title = {Zone Pricing in Retail Oligopoly}, | |
| 645 | + journal = {American Economic Journal: Microeconomics}, | |
| 646 | + year = {2019}, | |
| 647 | + volume = {11}, | |
| 648 | + number = {1}, | |
| 649 | + pages = {124--156}, | |
| 650 | + doi = {10.1257/mic.20170130} | |
| 651 | +} | |
| 652 | + | |
| 653 | +@article{sweeting2024, | |
| 654 | + author = {Butters, R. Andrew and Sacks, Daniel W. and Seo, Boyoung}, | |
| 655 | + title = {How Do National Firms Respond to Local Cost Shocks?}, | |
| 656 | + journal = {American Economic Review}, | |
| 657 | + year = {2022}, | |
| 658 | + volume = {112}, | |
| 659 | + number = {5}, | |
| 660 | + pages = {1737--1772}, | |
| 661 | + doi = {10.1257/aer.20201524} | |
| 662 | +} | |
| 663 | + | |
| 664 | +@article{gagnon2009, | |
| 665 | + author = {Gagnon, Etienne}, | |
| 666 | + title = {Price Setting During Low and High Inflation: Evidence from Mexico<sup>*</sup>}, | |
| 667 | + journal = {Quarterly Journal of Economics}, | |
| 668 | + year = {2009}, | |
| 669 | + volume = {124}, | |
| 670 | + number = {3}, | |
| 671 | + pages = {1221--1263}, | |
| 672 | + doi = {10.1162/qjec.2009.124.3.1221} | |
| 673 | +} | |
| 674 | + | |
| 675 | +@article{varian1980, | |
| 676 | + author = {Varian, Hal R.}, | |
| 677 | + title = {A Model of Sales}, | |
| 678 | + journal = {American Economic Review}, | |
| 679 | + year = {1980}, | |
| 680 | + volume = {70}, | |
| 681 | + number = {4}, | |
| 682 | + pages = {651--659}, | |
| 683 | + note = {https://www.jstor.org/stable/1805363} | |
| 684 | +} | |
| 685 | + | |
| 686 | +@article{feenstra1994, | |
| 687 | + author = {Feenstra, Robert C.}, | |
| 688 | + title = {New Product Varieties and the Measurement of International Prices}, | |
| 689 | + journal = {American Economic Review}, | |
| 690 | + year = {1994}, | |
| 691 | + volume = {84}, | |
| 692 | + number = {1}, | |
| 693 | + pages = {157--177}, | |
| 694 | + note = {https://www.jstor.org/stable/2117971} | |
| 695 | +} | |
| 696 | + | |
| 697 | +@article{midrigan2011, | |
| 698 | + author = {nan}, | |
| 699 | + title = {Menu Costs, Multiproduct Firms, and Aggregate Fluctuations}, | |
| 38 | 700 | journal = {Econometrica}, |
| 39 | − volume = {48}, | |
| 40 | − number = {4}, | |
| 41 | − pages = {817--838}, | |
| 42 | − year = {1980} | |
| 43 | −} | |
| 44 | − | |
| 45 | −@book{cochrane2005asset, | |
| 46 | − author = {Cochrane, John H.}, | |
| 47 | − title = {Asset Pricing}, | |
| 48 | − edition = {Revised}, | |
| 49 | − publisher = {Princeton University Press}, | |
| 50 | − year = {2005} | |
| 51 | −} | |
| 52 | − | |
| 53 | −@article{petersen2009estimating, | |
| 54 | − author = {Petersen, Mitchell A.}, | |
| 55 | − title = {Estimating Standard Errors in Finance Panel Data Sets: Comparing Approaches}, | |
| 56 | − journal = {Review of Financial Studies}, | |
| 57 | − volume = {22}, | |
| 58 | − number = {1}, | |
| 59 | − pages = {435--480}, | |
| 60 | − year = {2009} | |
| 701 | + year = {2011}, | |
| 702 | + volume = {79}, | |
| 703 | + number = {4}, | |
| 704 | + pages = {1139--1180}, | |
| 705 | + doi = {10.3982/ecta6735} | |
| 706 | +} | |
| 707 | + | |
| 708 | +@article{diewert2022, | |
| 709 | + author = {Diewert, W. Erwin and Fox, Kevin J.}, | |
| 710 | + title = {Substitution Bias in Multilateral Methods for CPI Construction}, | |
| 711 | + journal = {Journal of Business \& Economic Statistics}, | |
| 712 | + year = {2020}, | |
| 713 | + volume = {40}, | |
| 714 | + number = {1}, | |
| 715 | + pages = {355--369}, | |
| 716 | + doi = {10.1080/07350015.2020.1816176} | |
| 717 | +} | |
| 718 | + | |
| 719 | +@article{hortacsu2004, | |
| 720 | + author = {Hortacsu, A. and Syverson, C.}, | |
| 721 | + title = {Product Differentiation, Search Costs, and Competition in the Mutual Fund Industry: A Case Study of S\&P 500 Index Funds}, | |
| 722 | + journal = {The Quarterly Journal of Economics}, | |
| 723 | + year = {2004}, | |
| 724 | + volume = {119}, | |
| 725 | + number = {2}, | |
| 726 | + pages = {403--456}, | |
| 727 | + doi = {10.1162/0033553041382184} | |
| 728 | +} | |
| 729 | + | |
| 730 | +@techreport{competitionbureau2023, | |
| 731 | + author = {{Competition Bureau Canada}}, | |
| 732 | + title = {Canada Needs More Grocery Competition: Retail Grocery Market Study Report}, | |
| 733 | + institution = {Competition Bureau Canada}, | |
| 734 | + year = {2023}, | |
| 735 | + url = {https://competition-bureau.canada.ca} | |
| 736 | +} | |
| 737 | + | |
| 738 | +@article{cakir2021, | |
| 739 | + author = {Çakır, Metin}, | |
| 740 | + title = {Retail pass-through of package downsizing}, | |
| 741 | + journal = {Agribusiness}, | |
| 742 | + year = {2021}, | |
| 743 | + volume = {38}, | |
| 744 | + number = {2}, | |
| 745 | + pages = {259--278}, | |
| 746 | + doi = {10.1002/agr.21724} | |
| 747 | +} | |
| 748 | + | |
| 749 | +@article{hoppe2024, | |
| 750 | + author = {Melmiès, Jordan}, | |
| 751 | + title = {Hidden price increases, product downsizing, and shrinkflation: a post-Keynesian macroeconomic perspective}, | |
| 752 | + journal = {Industrial and Corporate Change}, | |
| 753 | + year = {2024}, | |
| 754 | + volume = {34}, | |
| 755 | + number = {2}, | |
| 756 | + pages = {318--341}, | |
| 757 | + doi = {10.1093/icc/dtae035} | |
| 758 | +} | |
| 759 | + | |
| 760 | +@article{chevalier2003, | |
| 761 | + author = {Chevalier, Judith A and Kashyap, Anil K and Rossi, Peter E}, | |
| 762 | + title = {Why Don't Prices Rise During Periods of Peak Demand? Evidence from Scanner Data}, | |
| 763 | + journal = {American Economic Review}, | |
| 764 | + year = {2003}, | |
| 765 | + volume = {93}, | |
| 766 | + number = {1}, | |
| 767 | + pages = {15--37}, | |
| 768 | + doi = {10.1257/000282803321455142} | |
| 769 | +} | |
| 770 | + | |
| 771 | +@article{hottman2016, | |
| 772 | + author = {Hottman, Colin J. and Redding, Stephen J. and Weinstein, David E.}, | |
| 773 | + title = {Quantifying the Sources of Firm Heterogeneity | |
| 774 | + *}, | |
| 775 | + journal = {The Quarterly Journal of Economics}, | |
| 776 | + year = {2016}, | |
| 777 | + volume = {131}, | |
| 778 | + number = {3}, | |
| 779 | + pages = {1291--1364}, | |
| 780 | + doi = {10.1093/qje/qjw012} | |
| 61 | 781 | } |
modified
paper/sections/conclusion.tex
+3 −10
@@ -1,12 +1,5 @@ | ||
| 1 | −% ============================================================================ | |
| 2 | −\section{Conclusion} | |
| 3 | −\label{sec:conclusion} | |
| 4 | −% ============================================================================ | |
| 1 | +\section{Conclusion}\label{sec:concl} | |
| 5 | 2 | |
| 6 | −This paper investigates \ldots{} Using data from \ldots{} over the period \ldots{}, we find that \ldots{} | |
| 3 | +Taking a price-comparison service's database as it is---posted prices, online channels, eleven weeks, no stores---we have tried to extract what it identifies and to say clearly what it does not. The architecture that emerges has four load-bearing elements. Banners differ by 10--25 log points on identical products, persistently, and mostly inside the two chains whose discount and conventional formats carry the same UPCs. Promotions are rare in catalogues but deep, recur every few weeks, account for half of all posted-price changes, double measured volatility, and return exactly to their reference price within the window. Cross-banner price co-movement is small and, where it is visible, is the calendar of a chain rather than a response to a rival. Measured ``inflation'' on catalogue data is driven by what enters and leaves the catalogue unless the index is matched-model. Underneath these sits a measurement fact that any user of scraped grocery prices should check for: a storefront that serves store-specific prices to anonymous sessions produces an oscillating series whose amplitude is cross-store dispersion, not price change. | |
| 7 | 4 | |
| 8 | −Our results contribute to the literature in several ways. First, \ldots{} Second, \ldots{} Third, \ldots{} | |
| 9 | − | |
| 10 | −These findings have important implications for \ldots{} From a policy perspective, \ldots{} For practitioners, \ldots{} | |
| 11 | − | |
| 12 | −Several limitations of the present study suggest directions for future research. First, \ldots{} Second, \ldots{} We leave these questions for future work. | |
| 5 | +A longer, store-identified, regular-price-logging collection---three changes to an existing pipeline---would turn several of this paper's descriptive statements into tests: whether reference prices drift after sales over a year, whether the two-point pre-sale elevation in clean feeds survives, how dispersion across stores of the same banner compares with dispersion across banners, and what a Quebec household at a given address can actually save. We have set the index, the matching procedure and the registration up so that those tests can be run on the same code. | |
modified
paper/sections/data.tex
+68 −23
@@ -1,32 +1,77 @@ | ||
| 1 | −% ============================================================================ | |
| 2 | −\section{Data} | |
| 3 | −\label{sec:data} | |
| 4 | −% ============================================================================ | |
| 1 | +\section{Data: audit, panel reconstruction and matching}\label{sec:data} | |
| 5 | 2 | |
| 6 | −\subsection{Data Sources} | |
| 7 | −\label{subsec:data_sources} | |
| 3 | +\subsection{Snapshot and provenance} | |
| 4 | +On 1 November 2026 at 07:57 UTC a consistent backup of the production SQLite database was taken through a read-only connection (SHA-256 recorded in the replication package; the Food-Ka code at commit \texttt{2c76523} was copied alongside to document the data-generating process). Production was not modified at any point. The snapshot holds 124{,}579 listings (50{,}760 active), 300{,}613 price-log rows, 15{,}630 connector passes over 57 feeds (median gap between passes of the same feed: 7.2 hours), 2{,}537 listings linked by Food-Ka's own cross-banner matcher, and auxiliary caches. Table~\ref{tab:coverage} summarises coverage for the largest feeds; Figure~\ref{fig:coverage} shows weekly coverage by channel and active listings by company. | |
| 8 | 5 | |
| 9 | −We obtain data from \ldots{} Our sample spans the period from January~1990 to December~2023, covering \ldots{} We exclude \ldots{} consistent with standard practice in the literature \citep{fama1993common}. | |
| 6 | +\begin{figure}[htbp]\centering | |
| 7 | +\includegraphics[width=\textwidth]{figures/fig_coverage.pdf} | |
| 8 | +\caption{Data coverage. A: listings observed per circular week by channel. B: active listings on 1 November 2026 by company (top 10). Source: Food-Ka snapshot of 1 November 2026; authors' computations.}\label{fig:coverage} | |
| 9 | +\end{figure} | |
| 10 | +\input{tables/tab_coverage_feeds} | |
| 10 | 11 | |
| 11 | −\subsection{Variable Construction} | |
| 12 | −\label{subsec:variables} | |
| 12 | +The composition is heavily skewed. Four feeds---Giant Tiger, Epipresto, La Boîte à Grains and Marché Nuvo---hold 52\% of active listings because their Shopify catalogues are read exhaustively, whereas the connectors of the large chains read selected aisles (Metro 1{,}540 active listings, Maxi 1{,}086, Walmart 922, Super~C 857, IGA 600, Provigo 450). The large chains nevertheless generate most of the price events (Metro alone 63{,}868; Club Entrepôt 68{,}438) and most of the deactivated listings, for reasons we turn to now. | |
| 13 | 13 | |
| 14 | −Our dependent variable is \ldots{} defined as: | |
| 15 | −% | |
| 16 | −\begin{equation} | |
| 17 | − r_{i,t} = \ln\!\left(\frac{P_{i,t}}{P_{i,t-1}}\right), | |
| 18 | − \label{eq:returns} | |
| 19 | −\end{equation} | |
| 20 | −% | |
| 21 | −where $P_{i,t}$ denotes the adjusted closing price of asset~$i$ at time~$t$. | |
| 14 | +\subsection{Source-to-variable inventory} | |
| 15 | +Table~\ref{tab:inventory} classifies each variable the mandate asked for. Product name, category (18 canonical classes), package label and current price are confirmed for all feeds; brand is missing for the Walmart, Adonis, T\&T, Akhavan, BocoBoco and Costco feeds (the brand sits inside the name); GS1-valid UPCs exist only for Metro and Super~C (100\% check-digit validity, 1{,}954 active listings), while Shopify, Loblaw and Walmart identifiers are internal SKUs (check-digit validity at the 10\% chance level). Package size is derivable for 63\% of active grocery listings after re-parsing labels and names (Food-Ka's own parser: 43\%); it is missing for most circular items and for Giant Tiger and Nuvo. Regular prices, promotion flags and stock status exist only as current state. Store and geography do not exist. Quantities and expenditure weights do not exist. | |
| 22 | 16 | |
| 23 | −The main explanatory variables include \ldots{} | |
| 17 | +\begin{table}[htbp]\centering\small | |
| 18 | +\begin{threeparttable} | |
| 19 | +\caption{Source-to-variable inventory}\label{tab:inventory} | |
| 20 | +\begin{tabular}{p{3.6cm}p{3.2cm}p{8.4cm}} | |
| 21 | +\toprule | |
| 22 | +Variable & Status & Notes \\ | |
| 23 | +\midrule | |
| 24 | +Product name & confirmed & all feeds; French/English mixed \\ | |
| 25 | +Brand & confirmed / partly unavailable & missing for 23\% of active listings (six feeds carry no brand field) \\ | |
| 26 | +UPC / GTIN & confirmed for 2 feeds & Metro, Super~C: GS1-valid; elsewhere internal SKUs; Open Food Facts barcodes attached by name lookup are derived and unreliable \\ | |
| 27 | +Package size, unit & derivable & re-parsed from label or name: 63\% of active grocery listings; missing mostly for circulars, Giant Tiger, Nuvo \\ | |
| 28 | +Category & confirmed & 18 canonical classes; ``Autres'' is a residual (19\%) \\ | |
| 29 | +Retailer / banner / company & derivable & 54 feeds $\to$ 43 banners with grocery listings $\to$ 26 companies \\ | |
| 30 | +Store & unavailable & no store identifier; storefronts answered anonymous sessions; circulars for one postal code \\ | |
| 31 | +Current price & confirmed & event log: first observation + every change \\ | |
| 32 | +Regular / reference price & current state only & historised nowhere; circulars carry a regular price per item-week \\ | |
| 33 | +Unit price & derivable & recomputed; per-kg labels, multi-buy labels flagged \\ | |
| 34 | +Promotion flag & current state only & historical state inferred from price paths (storefront) or implicit (circulars) \\ | |
| 35 | +Observation timestamp & confirmed (feed level) & 15{,}630 passes; listing-level presence between events inferred \\ | |
| 36 | +Circular validity & confirmed & valid-from / valid-to per item \\ | |
| 37 | +Availability / stock & unreliable & NULL for 23\% of active listings; current only \\ | |
| 38 | +Geography, quantities, expenditure weights & unavailable & --- \\ | |
| 39 | +\bottomrule | |
| 40 | +\end{tabular} | |
| 41 | +\begin{tablenotes}[flushleft]\footnotesize | |
| 42 | +\item Classification per the mandate: confirmed, derivable, unavailable, unreliable. Full inventory with per-feed rates in \texttt{reports/phase1\_data\_audit.md}. | |
| 43 | +\end{tablenotes} | |
| 44 | +\end{threeparttable} | |
| 45 | +\end{table} | |
| 24 | 46 | |
| 25 | −\subsection{Summary Statistics} | |
| 26 | −\label{subsec:summary_stats} | |
| 47 | +\subsection{Panel reconstruction} | |
| 48 | +Because the log records changes, a listing's price between two events is the last logged value, and the listing was observed at every pass of its feed between its first and last sighting. We reconstruct, for every listing and local day (America/Toronto), the duration-weighted distribution of displayed prices---mode, minimum, maximum, time-weighted mean, open and close, number of changes and number of distinct values---and aggregate to the Quebec circular week (Thursday to Wednesday). Two limitations are inherent: a listing that leaves and re-enters a feed without a price change leaves no trace, and deactivation requires two consecutive misses, so removals are dated to within one pass. Invariant tests verify that covered hours equal the observation span, that daily bounds hold and that single-event listings have a single price. The grocery universe (feeds other than pharmacies and the SAQ; categories other than health and beauty, household, pets, baby and alcohol; at least one observed price; no unrealistic price) spans 723{,}140 listing-weeks for 102{,}835 listings in 43 banners over the 11 complete weeks from 13 August to 22 October 2026 (the partial first and last weeks are dropped). | |
| 27 | 49 | |
| 28 | −Table~\ref{tab:summary_stats} presents the descriptive statistics for our key variables. | |
| 50 | +\subsection{A measurement finding: store-mixture noise}\label{sec:mixture} | |
| 51 | +Figure~\ref{fig:mixture}A plots every logged price of one Metro listing. The price alternates, within the same day, among \$5.77, \$6.99, \$7.49, \$7.79 and \$7.99. This is not repricing. In the feeds of Club Entrepôt, Metro, Maxi, Super~C, Provigo and Walmart, 48--68\% of logged changes revert to the previous-but-one value, the median listing with six or more events cycles among four distinct values, and the set of values observed in one week overlaps the next week's set with a median Jaccard index of 0.5. Single-store online shops (Shopify and WooCommerce catalogues) show none of this. The Food-Ka connectors for the large chains reach the sites through residential proxy pools without selecting a store; the sites then serve the price of whatever store they associate with the session. Each logged ``change'' in these feeds is therefore a draw from the banner's distribution of store prices. | |
| 29 | 52 | |
| 30 | −\input{tables/summary_stats} | |
| 53 | +\begin{figure}[htbp]\centering | |
| 54 | +\includegraphics[width=\textwidth]{figures/fig_store_mixture.pdf} | |
| 55 | +\caption{Store-mixture noise. A: all logged prices of one Metro listing (mayonnaise, 890 mL), 12 August -- 31 October 2026. B: share of listing-weeks in which more than one price value was displayed, by banner, with the median log range (max/min) on those weeks. Feeds behind anti-bot protection that serve store-specific prices (Club Entrepôt, Maxi, Metro, Super~C, Provigo, Walmart) stand apart from single-store online shops.}\label{fig:mixture} | |
| 56 | +\end{figure} | |
| 31 | 57 | |
| 32 | −Several patterns emerge from the summary statistics. First, \ldots{} Second, \ldots{} The standard deviations indicate \ldots{} | |
| 58 | +Three consequences follow and were fixed in the preregistration before any estimation. (i) The posted price of a listing in a week is defined as the duration-weighted \emph{modal} price, with minimum, maximum and mean as sensitivity definitions. (ii) Sub-weekly variation in these feeds is not interpreted as price flexibility; H2 is tested on weekly series and the falsification that single-store shops must not display the oscillation is part of the design. (iii) The within-week range of a listing in these feeds is a lower bound on within-banner, across-store dispersion for that product---a quantity we report (Table~\ref{tab:dispersion} note and Figure~\ref{fig:mixture}B) with the caveat that the stores are unidentified: 23\% of storefront listing-weeks in the anti-bot feeds display more than one price value, with a median log range of 0.15--0.24, against 4.7\% in single-store shops. | |
| 59 | + | |
| 60 | +\subsection{Attrition} | |
| 61 | +Table~\ref{tab:attrition} and Figure~\ref{fig:matching}A track listings through the filters. Of 124{,}579 listings, 111{,}575 belong to the grocery universe, 66{,}340 have a parsable package size and are eligible for matching, and 13{,}273 end up in a cross-banner group under tiers A--C (13{,}088 under the primary tiers A+B). The denominator of every cross-banner statistic in this paper is therefore about 11\% of listings, not the 50{,}760 products advertised on the home page. | |
| 62 | +\input{tables/tab_attrition} | |
| 63 | + | |
| 64 | +\subsection{Cross-banner matching}\label{sec:matching} | |
| 65 | +We match listings across banners in tiers of decreasing confidence. Tier~A is exact equality of GS1-valid UPCs (only Metro and Super~C). Tier~B requires the same brand key, the same package (total quantity in grams, millilitres or units \emph{and} the same multipack count, so that 12$\times$355~mL and 6$\times$710~mL are different packages), identical cleaned token sets after removing size tokens, stop words and the brand, and identical numeric tokens (``2\%'' versus ``3.25\%''). Tier~C relaxes token identity to a token-sort similarity of at least 0.90. Tier~D handles feeds without a brand field by requiring identical full-name token sets, the same package, and the presence of the branded side's brand tokens in the brand-less name. Size-less listings and multi-product circular offers (``X ou Y'', ``assortis'', ``sélectionnés'') are excluded from matching; a first version that allowed them produced chained groups of up to 141 listings. | |
| 66 | + | |
| 67 | +The thresholds were not chosen by eye. All 37{,}629 Metro$\times$Super~C candidate pairs sharing a brand and a package were scored and labelled by whether they share a UPC (4{,}331 positives). Token-set similarity---the scorer used by Food-Ka's own matcher and by a first version of ours---scores a name that is a subset of another at 1.0 (``beurre végétal salé'' matches ``beurre végétal non salé''); its precision at threshold 0.90 is 0.61. Identical token sets achieve precision 0.913 and recall 0.984; token-sort similarity $\ge 0.90$ cumulatively 0.845 and 0.989. The residual 8.7\% of identical-text pairs with different UPCs are packaging variants invisible in text, so 0.91 is a lower bound on the precision of tier~B relative to what a reader of the two listings could tell apart. | |
| 68 | + | |
| 69 | +Figure~\ref{fig:matching}B and Table~\ref{tab:validation} report the validation on a stratified random sample of 60 pairs per tier. Each pair was labelled by Claude (claude-opus-5-5) as a second annotator and then reviewed, pair by pair, by the research operator; the two agreed on all 240 pairs after review. Tiers A and B are correct in 60/60 cases each, tier~D in 59/60 (one unsure), tier~C in only 27/60: the token-sort rule admits flavour, roast, fat-content and colour variants (``douces'' vs.\ ``piquantes'', ``torréfaction moyenne'' vs.\ ``foncée''). The preregistration therefore fixes tiers A+B as the primary specification and uses C and D only in sensitivity analyses. The tiers yield 4{,}413 (A), 4{,}088 (B), 714 (C) and 102 (D) cross-banner pairs, and 5{,}909 groups under A+B covering 13{,}088 listings; the median group spans two banners and the largest seven. Against Food-Ka's own 2{,}033 linked pairs, our tiers recover 42.5\%---the rest are size-less or multi-offer circular links that we exclude by design. | |
| 70 | + | |
| 71 | +\begin{figure}[htbp]\centering | |
| 72 | +\includegraphics[width=\textwidth]{figures/fig_matching.pdf} | |
| 73 | +\caption{Matching validation and attrition. A: listing-level attrition flow. B: precision by tier from the 240-pair reviewed sample (Wilson 95\% intervals) and from UPC ground truth (Metro$\times$Super~C blocks; none for tier D).}\label{fig:matching} | |
| 74 | +\end{figure} | |
| 75 | +\input{tables/tab_match_validation} | |
| 76 | + | |
| 77 | +One structural fact emerges from the matched panel and conditions the interpretation of everything that follows: 93\% of matched group-weeks involve banners of the \emph{same} company (Metro vs.\ Super~C; Maxi vs.\ Provigo vs.\ Club Entrepôt). Exactly matched cross-company comparisons number 2{,}103 group-weeks. The apparent diversity of 54 banners collapses, for the purpose of like-for-like comparison, to format differentiation within two chains plus a thin layer of cross-chain pairs. | |
added
paper/sections/institutional.tex
+9 −0
@@ -0,0 +1,9 @@ | ||
| 1 | +\section{Institutional context: Quebec grocery retail and the Food-Ka source}\label{sec:inst} | |
| 2 | + | |
| 3 | +\paragraph{Market structure.} Grocery retail in Quebec is dominated by three vertically integrated companies operating several banners each: Metro Inc.\ (Metro, Super~C, Marché Adonis, the affiliated Richelieu, Marché Ami and Axep banners, and the Jean Coutu and Brunet pharmacies), Loblaw Companies (Maxi, Provigo, Club Entrepôt, Pharmaprix, T\&T Supermarket, and affiliated Inter-Marché stores) and Sobeys/Empire (IGA, Les Marchés Tradition, Marché Bonichoix, Rachelle-Béry). Walmart Canada and Costco add mass-merchant and warehouse-club formats; Giant Tiger is a discount general merchandiser with a large food assortment; a long tail of independent, ethnic, health-food and online-only grocers completes the market. The Société des alcools du Québec (SAQ) is a provincial monopoly with uniform posted prices and is excluded from all grocery analyses. Company affiliations used in the paper are recorded feed by feed in the \texttt{retailers} table with the basis for each attribution (public corporate information or the Food-Ka registry); they are descriptive and do not affect any estimate except the same-company/cross-company split. | |
| 4 | + | |
| 5 | +\paragraph{Channels.} Each large banner is present in Food-Ka through two \emph{feeds}: an online storefront (the e-commerce catalogue, read every $\approx$7 hours) and a weekly circular (read from the Flipp platform for the Montréal postal code H2X 1Y4, with validity dates, regular price and fine print). Smaller independents are present only through circulars; online-only grocers only through storefronts. Treating the 54 feeds as 54 retailers would therefore double count the large chains and conflate two very different price-setting objects: a catalogue price that applies to whatever store the site assigned to the session, and an advertised weekly promotion with an explicit regular price. We keep the two channels apart throughout. | |
| 6 | + | |
| 7 | +\paragraph{The Food-Ka source.} Food-Ka (\url{https://www.food-ka.com}) is an independent, non-commercial aggregator operated by the author. It maintains one connector per feed, normalises each listing to a common schema (name, brand, category, package label, price, regular price, unit price), computes a content hash and, at every pass, classifies each listing as new, modified or missing. Every change of the displayed price is appended to a log; a listing missing for two consecutive passes is deactivated. The public home page on 1 November 2026 reported 50{,}760 products, 54 banners and 8{,}631 products on promotion; we verified that these equal the number of \emph{active listings}, feeds with active listings and listings flagged on sale in the snapshot, and we stress that they are counts of source-feed listings, not of unique products: the same Coca-Cola 12-pack appears once per feed that carries it. | |
| 8 | + | |
| 9 | +\paragraph{What the source does and does not record.} Food-Ka was built to compare current prices, not to study price dynamics. Three consequences shape everything below. (i) It logs \emph{changes}, not observations: between two logged events a listing kept its price and was re-observed at every pass, but the pass-level presence of each listing is not stored. (ii) It historises the \emph{current} price only: regular prices, promotion flags and stock status are overwritten at each pass, so the promotion state of a storefront listing in the past must be inferred from its price path, while circular items carry an explicit regular price for their validity week. (iii) It has no store dimension: storefront connectors browse without selecting a store, and circulars are pulled for one postal code. These are the facts; the research design (Section~\ref{sec:methods}) is built around them rather than against them. | |
modified
paper/sections/introduction.tex
+10 −9
@@ -1,14 +1,15 @@ | ||
| 1 | −% ============================================================================ | |
| 2 | −\section{Introduction} | |
| 3 | −\label{sec:introduction} | |
| 4 | −% ============================================================================ | |
| 1 | +\section{Introduction}\label{sec:intro} | |
| 5 | 2 | |
| 6 | −The question of \ldots{} has attracted considerable attention in the finance and economics literature. Despite the growing body of evidence, the mechanisms underlying \ldots{} remain poorly understood. This paper contributes to this line of research by \ldots{} | |
| 3 | +Food inflation in Canada has become a political object. Between 2022 and 2024 food purchased from stores rose faster than the all-items Consumer Price Index, parliamentary committees summoned grocery executives, and the Competition Bureau concluded that the sector ``needs more competition'' \citep{competitionbureau2023}. Yet the public debate rests on a thin empirical layer: a monthly official index, a handful of average retail prices for selected products, and anecdotes about ``shrinkflation'' and dubious promotions. What a household actually pays for a comparable basket depends on which banner it visits, whether the item is on promotion that week, which package it buys, and how far it is willing to travel. None of these margins is visible in an aggregate index. | |
| 7 | 4 | |
| 8 | −The motivation for this study is threefold. First, \ldots{} Second, \ldots{} Third, \ldots{} | |
| 5 | +This paper asks a narrow, falsifiable set of questions about the \emph{architecture} of posted grocery prices in Quebec, using a data source that was not built for research but turns out to be unusually informative: Food-Ka, an independent price-comparison service that re-reads the public catalogues and weekly circulars of 57 grocery feeds every few hours and logs every price change it observes. We obtained a read-only copy of its production database on 1 November 2026. The copy contains 124{,}579 listings, 300{,}613 logged price events and 15{,}630 connector passes between 12 August and 1 November 2026. We treat this as what it is---eleven complete weeks of \emph{posted} prices from \emph{online} channels with no store identifiers, no transactions and no quantities---and we ask what can and cannot be learned from it. | |
| 9 | 6 | |
| 10 | −Our main findings can be summarized as follows. We show that \ldots{} Furthermore, we document that \ldots{} Finally, our robustness checks confirm that \ldots{} | |
| 7 | +Our contribution is threefold. First, we document a measurement phenomenon that we believe is new to the literature on scraped prices \citep{cavallo2016,cavallo2017,cavallo2018,gorodnichenko2017}: storefronts of the large chains answer anonymous sessions with the price of a store they assign to the session, so a high-frequency scraper that does not fix a store records a mixture of store prices. The signature is unmistakable---a Metro listing of mayonnaise cycles between \$5.77, \$6.99, \$7.49, \$7.79 and \$7.99 within a single day, more than half of logged ``changes'' revert to the previous-but-one value, and the phenomenon is absent from single-store online shops (Section~\ref{sec:data}). We show how to extract a defensible weekly posted price from such data and how the oscillation amplitude itself bounds within-banner, across-store dispersion. | |
| 11 | 8 | |
| 12 | −This paper relates to several strands of the literature. First, it contributes to the literature on \ldots{} \citep{fama1993common}. Second, it builds on the work of \citet{bollerslev1986generalized} by \ldots{} Third, our methodological approach draws on \citet{newey1987simple}. | |
| 9 | +Second, we build and validate a cross-banner matching procedure. Only the Metro Inc.\ feeds expose GS1-valid UPCs; everything else requires probabilistic linkage. We calibrate the text rules on 37{,}629 candidate pairs between Metro and Super~C whose shared UPC provides ground truth, obtaining a rule (identical brand, package and cleaned token set) with precision 0.91 and recall 0.98 on that labelled set, and we validate it on a 240-pair stratified sample reviewed pair by pair. A looser rule that a first version of the pipeline used (token-\emph{set} similarity) was rejected after validation; we report it. The result is 5{,}909 cross-banner product groups---a few per cent of all listings, which is the honest denominator for any ``price comparison'' claim about this market. | |
| 13 | 10 | |
| 14 | −The remainder of the paper is organized as follows. Section~\ref{sec:literature} reviews the related literature. Section~\ref{sec:data} describes the data. Section~\ref{sec:methodology} presents the empirical methodology. Section~\ref{sec:results} reports the main results. Section~\ref{sec:robustness} provides robustness checks. Section~\ref{sec:conclusion} concludes. | |
| 11 | +Third, on this matched panel we estimate the preregistered hypotheses H1--H6 of our registration (frozen before estimation; Section~\ref{sec:methods}). On identical products in the same week the median across-banner log price gap is 0.154, dispersion is persistent (banner-pair gaps have an AR(1) coefficient of 0.66 against a permutation placebo of 0.00), and banner fixed effects span 32 log points. Inferred promotions are rare in storefront catalogues (4.6\% of listing-weeks) but deep (median 22\%), account for half of all weekly price changes and double measured volatility. Temporary sales return exactly to their pre-sale reference price---we find no reference-price drift within the window---while a pre-sale elevation of the posted price is largely a store-mixture artefact. Lead--lag associations between rivals' price changes are small, mostly within company, and fail a lead placebo for cross-company pairs; we therefore make no causal claim about competitive responses. Matched-model and naive indices agree closely at the market level over eleven weeks, but diverge by 10--30 points for individual banners with assortment turnover. A fixed-basket optimisation shows that unconstrained cheapest-banner shopping would save about 15\% relative to the best single banner and that two banners capture 85\% of that; travel costs and store access are unobserved and enter only as labelled assumptions. We assemble these pieces into a transparent five-component Quebec Grocery Price and Affordability Index (QGPAI) whose fifth component---access and affordability---is deliberately left empty. | |
| 12 | + | |
| 13 | +Throughout we separate what the data identify from what they suggest. Posted prices are not paid prices; a listing that disappears has not been sold; a ``sale'' tag is not a discount. The eleven-week window is too short for several of the registered tests (notably H3 on reference-price adjustment and the validation of QGPAI against official indices), and we say so rather than stretch the estimand. Section~\ref{sec:limits} and the prospective collection plan in Section~\ref{sec:repro} describe what a longer, store-identified panel would add. | |
| 14 | + | |
| 15 | +The remainder of the paper is organised as follows. Section~\ref{sec:lit} reviews the literature on price dispersion, sales and scraped prices; Section~\ref{sec:inst} describes the Quebec grocery sector and the Food-Ka source; Section~\ref{sec:data} audits the data and constructs the panel and the matches; Section~\ref{sec:theory} sets out a simple model whose observable implications we test; Section~\ref{sec:methods} states the preregistered designs; Sections~\ref{sec:results} and~\ref{sec:robust} report results and robustness; Sections~\ref{sec:policy}--\ref{sec:concl} discuss implications, limitations and conclusions; Section~\ref{sec:repro} documents reproducibility and data access. | |
added
paper/sections/limitations.tex
+13 −0
@@ -0,0 +1,13 @@ | ||
| 1 | +\section{Limitations}\label{sec:limits} | |
| 2 | + | |
| 3 | +\begin{enumerate} | |
| 4 | +\item \textbf{Posted, not paid, prices.} We observe catalogue and circular prices, not transactions, quantities or expenditure weights. Nothing here is a welfare measure or a consumer price index. | |
| 5 | +\item \textbf{Eleven weeks.} The window is too short for the registered minimum durations of H3 (26 weeks) and of the index validation against official statistics (52 weeks); seasonal patterns are invisible; the promotion recurrence interval is right-censored. | |
| 6 | +\item \textbf{No store identifiers.} Storefront prices are mixtures over unknown stores; circulars are for one Montréal postal code; the ``nearby stores'' part of H4, the travel-cost part of H6 and QGPAI component 5 cannot be computed. The prospective design in Section~\ref{sec:repro} addresses this. | |
| 7 | +\item \textbf{Inferred promotion states.} Regular prices are not historised for storefronts, so promotion states rest on a 10\%-below-trailing-mode rule validated only against the current on-sale flag (agreement 0.90) and the circular depth distribution. Small reductions and loyalty prices are classified as regular. | |
| 8 | +\item \textbf{Matching coverage and error.} Exactly matched products are 11\% of listings and 93\% of matched group-weeks are within-company pairs. Tier~B carries an estimated 9\% rate of hidden-variant false matches on UPC ground truth; the reviewed sample is small (60 pairs per tier) and its final labels were set by the research operator rather than by independent human coders; PI adjudication is pending. | |
| 9 | +\item \textbf{Assortment coverage.} The connectors of the large chains read a subset of aisles; Giant Tiger, Epipresto, La Boîte à Grains and Nuvo are read exhaustively. Market-level statistics are therefore not representative of the grocery basket's expenditure shares. | |
| 10 | +\item \textbf{Package changes unobserved.} Size labels are stored as current state; within-listing downsizing cannot be detected. | |
| 11 | +\item \textbf{Weak external benchmark.} Two monthly CPI observations overlap the window. | |
| 12 | +\item \textbf{Analytic amendments.} Three post-registration amendments (A-001 to A-003) were required; each is logged with the preregistered alternative. | |
| 13 | +\end{enumerate} | |
modified
paper/sections/literature.tex
+10 −14
@@ -1,21 +1,17 @@ | ||
| 1 | −% ============================================================================ | |
| 2 | −\section{Literature Review} | |
| 3 | −\label{sec:literature} | |
| 4 | −% ============================================================================ | |
| 1 | +\section{Related literature}\label{sec:lit} | |
| 5 | 2 | |
| 6 | −This section reviews the main theoretical and empirical contributions related to our research question. | |
| 3 | +\paragraph{Price dispersion and search.} That identical goods sell at different prices in the same market is the oldest puzzle of the economics of information \citep{stigler1961}. Equilibrium dispersion arises when some consumers are informed and others are not \citep{salop1977}, or when consumers sample a small number of prices \citep{burdett1983}; \citet{baye2006} survey the theory and the evidence. Empirically, dispersion is large and persistent even for homogeneous products \citep{sorensen2000,lach2002}, and most of it is \emph{relative} dispersion---the same store is cheap for some goods and expensive for others---rather than a stable store-level ranking \citep{kaplan2015,kaplan2019}. Our H1 tests whether the Quebec banner-level posted prices display economically meaningful and persistent gaps on exactly matched items; the within-banner store mixture we uncover connects directly to the store-level dispersion these papers measure with scanner data. | |
| 7 | 4 | |
| 8 | −\subsection{Theoretical Framework} | |
| 9 | −\label{subsec:theory} | |
| 5 | +\paragraph{Sales, high--low pricing and reference prices.} \citet{varian1980} rationalises temporary sales as a mixed strategy that price-discriminates between informed shoppers and loyal customers; \citet{pesendorfer2002} and \citet{hendel2006,hendel2013} show how storable-goods demand accumulation generates sale cycles; \citet{hosken2004} document the empirical regularities of retail price variation; \citet{chevalier2003} and \citet{macdonald2000} show prices falling at demand peaks; \citet{chevalier2019} measure how consumers exploit ``best prices''. The macro literature treats sales as distinct from regular-price changes: regular prices are sticky, sales are frequent, temporary and return to the reference \citep{bils2004,nakamura2008,klenow2008,eichenbaum2011,kehoe2015}, sales respond weakly to aggregate shocks \citep{guimaraes2011,coibion2015,anderson2017,gagnon2020}, and menu-cost models with multi-product firms reproduce the size distribution of changes \citep{golosov2007,midrigan2011,nakamura2013,anderson2015}. Our H2 and H3 are the posted-price implications of this literature: volatility with and without sales, and whether the reference price moves after a sale. | |
| 10 | 6 | |
| 11 | −The theoretical foundations of \ldots{} were established by \citet{cochrane2005asset}. In their framework, \ldots{} | |
| 7 | +\paragraph{Retail competition and chain pricing.} Retailers choose between every-day-low-price and high--low formats \citep{hoch1994,lal1997,ellickson2008}; large chains price nearly uniformly across their stores \citep{dellavigna2019,hitsch2019,hitsch2021}, with zone pricing as the main exception \citep{adams2024}; entry of mass merchants lowers measured prices \citep{hausman2007}. The Canadian retail grocery market is concentrated among three national chains plus Walmart and Costco \citep{competitionbureau2023}. We map Food-Ka's 54 feeds to 26 companies and show that most exactly matched comparisons are \emph{within} company (Metro vs.\ Super~C; Maxi vs.\ Provigo vs.\ Club Entrepôt)---format differentiation inside a chain---rather than across chains. | |
| 12 | 8 | |
| 13 | −\subsection{Empirical Evidence} | |
| 14 | −\label{subsec:empirical_evidence} | |
| 9 | +\paragraph{Scraped and online prices.} The Billion Prices Project established that online prices are a valid high-frequency source for inflation measurement and that online and offline prices coincide for most items of multi-channel retailers \citep{cavallo2016,cavallo2017}; scraped data reveal more frequent price changes and stockouts than official collections \citep{cavallo2018,cavallo2023,gorodnichenko2017}. Our contribution to this literature is cautionary: when a retailer's site serves store-specific prices and the scraper does not pin the store, the ``online price'' is a draw from the chain's store-price distribution. | |
| 15 | 10 | |
| 16 | −A growing body of empirical literature examines \ldots{} \citet{fama1993common} document that \ldots{} More recently, \citet{petersen2009estimating} show that \ldots{} | |
| 11 | +\paragraph{Price indices, substitution and shrinkflation.} Matched-model and multilateral methods address chain drift and assortment change \citep{feenstra1994,ivancic2011,dehaan2011,melser2018,diewert2022,redding2020}; price measurement during the pandemic highlighted the role of changing baskets \citep{diewert2020}; household-level differences in inflation arise from product-level heterogeneity \citep{jaravel2019,argente2021,beck2024,hottman2016}. Package downsizing is a hidden price increase to which demand is less sensitive than to list-price increases \citep{gourville2004,cakir2014,cakir2021,hoppe2024}. Our H5 compares naive average posted prices with matched-model indices on the same data; we cannot observe within-listing package changes and say so. | |
| 17 | 12 | |
| 18 | −\subsection{Contribution to the Literature} | |
| 19 | −\label{subsec:contribution} | |
| 13 | +\paragraph{Shopping effort, store choice and access.} Paid prices fall with shopping time and store switching \citep{aguiar2007,griffith2009,kaplan2016,nevo2019}; private labels and brand premia reflect information as much as quality \citep{bronnenberg2015,dube2018}; spatial access and assortment shape effective prices \citep{handbury2015,allcott2019,atkin2018}. Our H6 computes the posted-price savings frontier under a banner-count constraint; because store geography is unobserved, travel costs enter only as an illustrative sensitivity. | |
| 20 | 14 | |
| 21 | −Our paper departs from the existing literature in several important ways. First, \ldots{} Second, \ldots{} Third, \ldots{} | |
| 15 | +\paragraph{Record linkage.} Probabilistic matching of product listings is a record-linkage problem \citep{fellegi1969,christen2012}; recording error in scanner panels is itself a research topic \citep{einav2010}. We treat matching as a measured component of the design, with tier-specific precision estimates propagated to the results. | |
| 16 | + | |
| 17 | +The evidence matrix in the replication package (\texttt{literature/evidence\_matrix.csv}) links each theoretical claim used in this paper to these sources, states the limitation for our setting, and identifies where WP37 adds something. | |
deleted
paper/sections/methodology.tex
+0 −44
@@ -1,44 +0,0 @@ | ||
| 1 | −% ============================================================================ | |
| 2 | −\section{Methodology} | |
| 3 | −\label{sec:methodology} | |
| 4 | −% ============================================================================ | |
| 5 | − | |
| 6 | −\subsection{Empirical Model} | |
| 7 | −\label{subsec:model} | |
| 8 | − | |
| 9 | −To test our hypotheses, we estimate the following baseline specification: | |
| 10 | −% | |
| 11 | −\begin{equation} | |
| 12 | − y_{i,t} = \alpha + \beta_1 X_{i,t} + \beta_2 Z_{i,t} + \gamma_i + \delta_t + \varepsilon_{i,t}, | |
| 13 | − \label{eq:baseline} | |
| 14 | −\end{equation} | |
| 15 | −% | |
| 16 | −where $y_{i,t}$ is the dependent variable for firm~$i$ in period~$t$, $X_{i,t}$ is the main variable of interest, $Z_{i,t}$ is a vector of control variables, $\gamma_i$ represents firm fixed effects, $\delta_t$ captures time fixed effects, and $\varepsilon_{i,t}$ is the error term. | |
| 17 | − | |
| 18 | −Our coefficient of interest is $\beta_1$. Under the null hypothesis that \ldots{}, we expect $\beta_1 = 0$. A positive (negative) estimate of $\beta_1$ would indicate \ldots{} | |
| 19 | − | |
| 20 | −\subsection{Identification Strategy} | |
| 21 | −\label{subsec:identification} | |
| 22 | − | |
| 23 | −The main identification challenge arises from \ldots{} To address potential endogeneity concerns, we employ \ldots{} | |
| 24 | − | |
| 25 | −We cluster standard errors at the firm level to account for within-firm serial correlation, following \citet{petersen2009estimating}. | |
| 26 | − | |
| 27 | −\subsection{Extended Specifications} | |
| 28 | −\label{subsec:extended} | |
| 29 | − | |
| 30 | −We extend the baseline model in several directions. First, we allow for nonlinear effects: | |
| 31 | −% | |
| 32 | −\begin{equation} | |
| 33 | − y_{i,t} = \alpha + \beta_1 X_{i,t} + \beta_2 X_{i,t}^2 + \boldsymbol{\beta}_3' \mathbf{Z}_{i,t} + \gamma_i + \delta_t + \varepsilon_{i,t}. | |
| 34 | − \label{eq:nonlinear} | |
| 35 | −\end{equation} | |
| 36 | − | |
| 37 | −Second, we examine heterogeneous effects by interacting $X_{i,t}$ with \ldots{}: | |
| 38 | −% | |
| 39 | −\begin{equation} | |
| 40 | − y_{i,t} = \alpha + \beta_1 X_{i,t} + \beta_2 D_{i,t} + \beta_3 (X_{i,t} \times D_{i,t}) + \boldsymbol{\beta}_4' \mathbf{Z}_{i,t} + \gamma_i + \delta_t + \varepsilon_{i,t}, | |
| 41 | − \label{eq:interaction} | |
| 42 | −\end{equation} | |
| 43 | −% | |
| 44 | −where $D_{i,t}$ is an indicator variable equal to one if \ldots{} The coefficient $\beta_3$ captures the differential effect of $X$ for \ldots{} | |
added
paper/sections/methods.tex
+19 −0
@@ -0,0 +1,19 @@ | ||
| 1 | +\section{Identification and preregistered methods}\label{sec:methods} | |
| 2 | + | |
| 3 | +The hypotheses, samples, estimands, nulls, clustering and falsification tests were written down and hashed (\texttt{research/preregistration.md}) after the data audit and before any cross-banner gap, promotion statistic, index, response model or forecast was computed. The registration also lists what had been examined before freezing (table counts, coverage, the oscillation diagnostics, the distribution of current discount depth, matching validation). Three amendments were made during estimation and are logged with their rationale (\texttt{research/decisions.md}): a complete-block design for the basket problem (A-001), a unit-price screen on basket candidates (A-002), and a classification of low-price episodes into temporary sales and regular-price resets (A-003). None changes an estimand; all are reported with the preregistered version where it differs. | |
| 4 | + | |
| 5 | +\paragraph{Common definitions.} Posted price $p_{irw}$ of listing $i$ (banner $r$) in week $w$ is the duration-weighted modal price. A \emph{matched group} is a connected component of tier A+B pairs; group-weeks with fewer than two banners posting, or with more than one simultaneously posted listing from the same banner-channel (``ambiguous'', 2.2\%), are dropped. The storefront channel is primary; circulars are added in sensitivity analyses. Standard errors are clustered by match group; a two-way group$\times$banner clustering is reported for banner effects (with 14 banners carrying matched storefront listings the banner dimension is small, which we flag). | |
| 6 | + | |
| 7 | +\paragraph{Model A (H1).} We estimate $\log p_{irw}=\alpha_{iw}+\gamma_r+\varepsilon_{irw}$ with product-week and banner fixed effects on 67{,}732 listing-weeks. The $\gamma_r$ are relative posted-price levels for identical products in the same week; the residual standard deviation measures dispersion not explained by banner levels. Dispersion statistics per group-week (max--min log gap, coefficient of variation, interquartile range) are summarised overall, by company relation and by category. Persistence is the pooled AR(1) coefficient of banner-pair gaps $g_{i,rs,w}=\log p_{irw}-\log p_{isw}$ on their own one-week lag and the share of pairs ($\ge 4$ weeks) whose sign never flips; the placebo permutes banner labels within group-week (20 replications), which must drive the AR(1) to zero if persistence is a property of banners and not of the construction. The registered economic threshold is a median gap of at least 5\% and AR(1) of at least 0.5. | |
| 8 | + | |
| 9 | +\paragraph{Model B (H2, H3).} For storefront listings observed in at least 8 of 11 weeks, the reference price in week $w$ is the modal price over the trailing eight weeks (ties resolved to the higher price); a week is \emph{promoted} if $p_w\le 0.90\,\bar p_w$. This is a posted-price version of the sales filters of \citet{nakamura2008} and \citet{kehoe2015}; it is validated against the current on-sale flag in the last week (agreement 0.90). H2 compares the standard deviation of weekly log changes of the posted price with that of the reference series, overall, by banner and by category, with a listing-level bootstrap (999 draws) for the ratio. Episodes are maximal runs of promoted weeks; censored episodes (touching the panel edges) are excluded from duration and post-episode statistics. Under amendment A-003 an episode is a \emph{return} if the first post-episode price is within $\pm$2\% of the pre-episode reference, a \emph{reset down} if it is lower, a \emph{reset up} if higher. H3 asks whether the reference after a return episode differs from the reference before it; the event study regresses $\log p$ on week-relative-to-start dummies ($-4\ldots+4$, reference $-1$) with listing and week fixed effects and tests the joint nullity of the pre-period dummies; a placebo draws pseudo-episode dates in never-promoted listings. The falsification registered for the store-mixture interpretation---single-store shops must not display multi-price weeks---is reported with the H2 results, and the event study is re-estimated separately for single-store and anti-bot feeds. | |
| 10 | + | |
| 11 | +\paragraph{Model C (H4).} Store geography does not exist, so the ``nearby stores'' part of H4 is untestable and is not tested. For the banner-level part we regress $\Delta\log p_{irw}$ on the mean change of the other banners in the group at lags 0--3 and leads 1--2, with banner and week fixed effects, separately for all rivals, same-company rivals and other-company rivals. Contemporaneous coefficients absorb common shocks and are not causal; lags are lead--lag associations; leads are a placebo. No instrument or quasi-experiment is available and none is used. | |
| 12 | + | |
| 13 | +\paragraph{Model D (H5).} On the grocery universe we compute, weekly, (a) the naive mean and median of posted prices, (b) a chained Jevons matched-model index (geometric mean of week-on-week ratios over listings present in both weeks), (c) fixed-basket Dutot, Carli and Jevons indices over listings present in all eleven weeks, with equal item weights, and (d) unit-price versions. No expenditure weights exist, so none of these is a consumer price index and we do not call them one. The gap between (a) and (b) is bootstrapped over listings. Because the package label is stored as current state only, within-listing size changes cannot be observed; the H5 test is therefore restricted to the composition (assortment) channel. Statistics Canada's CPI for food purchased from stores in Quebec (table 18-10-0004-01) is compared over the two months of overlap, August to September 2026, as a check of coherence, not of superiority. | |
| 14 | + | |
| 15 | +\paragraph{Model E (H6).} A 30-item basket with reference quantities was fixed before any basket cost was computed (\texttt{config/basket.yaml}). Items are mapped to listings by category and name rule; listings with a unit price outside one quarter to four times the item's cross-banner median are dropped (A-002). For each week, the item cost at a banner is the cheapest unit price times the reference quantity. Under A-001 the banner set is the $B$ widest-coverage storefront banners and the item set the items all of them carry that week ($B=5$ primary: Super~C, Metro, Giant Tiger, Walmart, Marché Nuvo or IGA depending on the week; $B=4,6,7$ in sensitivity), so that the single-banner problem is feasible without imputation. The mixed-integer program assigns each item to one banner, opens at most $K$ banners, and minimises cost; $K=1$ is the best single banner, $K=B$ the unconstrained optimum. Travel costs, store access, loyalty programmes and stock are unobserved; a fixed cost per banner visited of 5, 10 and 20~CAD is added as a labelled illustration. For the QGPAI trajectory a block that is constant over weeks (five banners, ten items) is used. | |
| 16 | + | |
| 17 | +\paragraph{Model F.} Next-week log posted price and next-week promotion state of storefront listings are forecast with strict rolling origins ($t=6,\dots,9$; train on weeks $\le t-1$, test on week $t$), lag-only features, and four model classes: last value, banner$\times$category drift, ridge regression, gradient-boosted trees; a logistic model and a persistence rule for the promotion target. Metrics are MAE/RMSE and Brier/AUC. | |
| 18 | + | |
| 19 | +\paragraph{Multiple testing.} The six families H1--H6 each carry one or two primary tests; Holm's correction is applied across them (Table~\ref{tab:holm}). | |
added
paper/sections/policy.tex
+11 −0
@@ -0,0 +1,11 @@ | ||
| 1 | +\section{Policy and practical implications}\label{sec:policy} | |
| 2 | + | |
| 3 | +The results are descriptive and the window is short, so implications are stated as what the evidence supports and what it would take to go further. | |
| 4 | + | |
| 5 | +\emph{For consumers and comparison services.} On identical products the choice of banner moves the posted price by 10--25 log points, and a shopper restricted to two banners captures most of the attainable posted-price saving. A comparison service that reports the \emph{weekly modal} price, flags multi-price weeks and labels same-company pairs would convey this architecture more faithfully than one that reports the last scraped price. | |
| 6 | + | |
| 7 | +\emph{For statistical agencies and researchers using web-scraped prices.} Store-selected storefronts are a mixture of store prices unless the collector pins the store. The Billion-Prices-style assumption that one online price represents the retailer does not hold for Quebec's large chains; high-frequency ``changes'' in such feeds overstate price flexibility by an order of magnitude, and the correct unit of observation is a store-identified session or, failing that, a weekly modal price with the range reported. | |
| 8 | + | |
| 9 | +\emph{For competition policy.} The like-for-like evidence is overwhelmingly \emph{within} company: the Loblaw and Metro discount banners price 10--18 log points below their conventional banners on the same UPCs. This is consistent with deliberate format segmentation inside concentrated groups, which may be efficient (serving heterogeneous consumers) or may be a substitute for cross-chain competition; posted prices cannot tell these apart. Cross-chain exactly matched comparisons are rare in this source because chains differentiate assortments and private labels, which is itself a finding about how competition operates. | |
| 10 | + | |
| 11 | +\emph{For the measurement of food inflation.} Over eleven weeks the matched-model storefront index and the naive average agreed at the market level, but naive averages for individual banners were off by 10--30 points because of assortment turnover. Any index built on catalogue data must be a matched-model index; any claim about ``shrinkflation'' requires storing package labels over time, which this source did not do and which is a one-line change in the collection design. | |
added
paper/sections/reproducibility.tex
+9 −0
@@ -0,0 +1,9 @@ | ||
| 1 | +\section{Reproducibility, data access and prospective design}\label{sec:repro} | |
| 2 | + | |
| 3 | +\paragraph{Code and outputs.} The pipeline (Python 3.12; pinned dependencies) runs from the raw snapshot to the compiled paper with \texttt{make}: ingestion to Parquet with checksum verification, unit normalisation, retailer hierarchy, daily and weekly panel reconstruction (DuckDB), tiered matching with UPC calibration and LLM second annotation, Models A--F, QGPAI, robustness, LaTeX tables and vector figures. Every stochastic step uses seed 20261101. Each stage writes a manifest with SHA-256 hashes of its outputs and a line in a structured run ledger; model scripts write JSON summaries from which all numbers in the text and tables are generated. Fifteen tests (unit parsing, panel invariants, matching rules, output consistency) pass. The preregistration and basket were hashed before estimation; the decision log records three amendments. The LLM second annotation (claude-opus-5-5, three runs, 1.21~USD, model and tokens logged) is auxiliary: its labels are stored, so the pipeline runs without API access. | |
| 4 | + | |
| 5 | +\paragraph{Data access.} The raw snapshot is a read-only export of a database owned by the author and is not redistributed in the repository; it can be made available to referees under a data-use agreement, together with the SHA-256 of the file used here. Food-Ka's \texttt{robots.txt} disallows automated access to its API; the public API was queried once to cross-check headline counts and is not a data source. Statistics Canada series were obtained from the public Web Data Service (tables 18-10-0004-01 and 18-10-0245-01; raw JSON kept with download timestamps). | |
| 6 | + | |
| 7 | +\paragraph{Prospective collection design (not implemented here; no production change was made).} (i) Store selection: pin a fixed panel of about twenty stores across Quebec regions in the Loblaw, Metro and Walmart connectors and pull Flipp circulars for at least six postal codes (Montréal, Québec, Gatineau, Sherbrooke, Saguenay, Trois-Rivières), one pass per store per day. (ii) Regular-price logging: add regular price and on-sale flag to the price log. (iii) Package-label logging: keep a history of size labels to detect downsizing. (iv) Duration: 26 weeks for promotion recurrence and reference-price tests, 52 weeks for seasonality and validation against official indices. The authorisation is the owner's; the changes are reversible and polite. | |
| 8 | + | |
| 9 | +\paragraph{Use of AI assistance.} The pipeline, the analyses and the drafting of this manuscript were produced by the author with an AI research assistant (Claude Code) operating under the written mandate reproduced in the repository; all design decisions, amendments and the validation review are documented and the author is responsible for the content. No coauthor or institutional approval is implied. | |
modified
paper/sections/results.tex
+73 −17
@@ -1,27 +1,83 @@ | ||
| 1 | −% ============================================================================ | |
| 2 | −\section{Results} | |
| 3 | −\label{sec:results} | |
| 4 | −% ============================================================================ | |
| 1 | +\section{Empirical results}\label{sec:results} | |
| 5 | 2 | |
| 6 | −\subsection{Baseline Results} | |
| 7 | −\label{subsec:baseline_results} | |
| 3 | +\subsection{H1: Matched-item dispersion is large and persistent} | |
| 4 | +Table~\ref{tab:dispersion} and Figure~\ref{fig:dispersion}A report the distribution of within-product, cross-banner log price gaps on 30{,}042 group-weeks. The median max--min gap is 0.154 (interquartile range 0.059--0.279, mean 0.189); 79\% of group-weeks exceed the registered 5\% threshold and 63\% exceed 10\%. The median coefficient of variation is 0.073. Gaps are somewhat larger across companies (median 0.183 on 2{,}103 group-weeks) than within a company (0.151), but the within-company gaps---the discount banner against the conventional supermarket of the same chain on the same UPC---are the bulk of the evidence and are far from zero. | |
| 5 | +\input{tables/tab_dispersion} | |
| 8 | 6 | |
| 9 | −Table~\ref{tab:regression} presents the estimation results of our baseline model specified in Equation~\eqref{eq:baseline}. Column~(1) reports the OLS estimates without fixed effects. Columns~(2)--(4) progressively add firm and time fixed effects and the full set of controls. | |
| 7 | +\begin{figure}[htbp]\centering | |
| 8 | +\includegraphics[width=\textwidth]{figures/fig_dispersion.pdf} | |
| 9 | +\caption{Cross-banner dispersion on identical products (tiers A+B, storefront). A: distribution of the group-week max--min log gap by company relation. B: banner fixed effects relative to Metro with 95\% intervals (banners with at least 50 matched listing-weeks; standard errors clustered by match group).}\label{fig:dispersion} | |
| 10 | +\end{figure} | |
| 10 | 11 | |
| 11 | −\input{tables/regression} | |
| 12 | +The banner fixed effects (Table~\ref{tab:bannerfe}, Figure~\ref{fig:dispersion}B) are the price-level architecture of the market as seen through identical products: relative to Metro, Maxi posts 18.4 log points lower (s.e.\ 0.016), Club Entrepôt 13.6 lower, Super~C 10.0 lower (s.e.\ 0.003, on 17{,}329 listing-weeks), Provigo 4.3 lower; Walmart and Giant Tiger are 20 and 19 points lower but on few matched listing-weeks; specialty and online shops (La Boîte à Grains, Marché Nuvo, Maturin) post 8--12 points higher. The residual standard deviation after product-week and banner effects is 0.115: knowing the banner explains the level, but a product-specific component of comparable size remains---consistent with the ``relative dispersion'' emphasised by \citet{kaplan2019}. | |
| 13 | +\input{tables/tab_banner_effects} | |
| 12 | 14 | |
| 13 | −The coefficient on $X_{i,t}$ is positive and statistically significant at the 1\% level across all specifications. The point estimate in our preferred specification (Column~4) implies that a one-standard-deviation increase in $X$ is associated with a \ldots{} increase in $y$. | |
| 15 | +Persistence is strong. The pooled AR(1) coefficient of banner-pair gaps is 0.663 (s.e.\ 0.014); permuting banner labels within group-week gives 0.004 (s.d.\ 0.011 over 20 permutations). The coefficient is similar within (0.668) and across (0.630) companies. Among 4{,}884 banner pairs observed in at least four weeks, 46\% keep the same sign throughout. Part of the persistence reflects the stickiness of posted prices themselves---a gap that does not change because neither price changed---but the placebo shows that it is a property of banners, not of the construction, and the sign stability indicates that for close to half of the pairs one banner is systematically cheaper. Figure~\ref{fig:heatmap} displays the median pairwise gaps for banner pairs with at least 30 common group-weeks: the Loblaw discount banners (Maxi, Club Entrepôt) and Super~C are cheaper than Metro and Provigo by 10--26 log points, while Mayrand and the health-food shops are dearer. | |
| 14 | 16 | |
| 15 | −\subsection{Graphical Evidence} | |
| 16 | −\label{subsec:graphical} | |
| 17 | +\begin{figure}[htbp]\centering | |
| 18 | +\includegraphics[width=0.85\textwidth]{figures/fig_heatmap.pdf} | |
| 19 | +\caption{Pairwise median log price gaps on identical products (row banner minus column banner), tiers A+B, storefront channel, pairs with at least 30 group-weeks. Blue: row banner cheaper.}\label{fig:heatmap} | |
| 20 | +\end{figure} | |
| 17 | 21 | |
| 18 | −Figure~\ref{fig:main_result} provides graphical evidence supporting our main findings. | |
| 22 | +Category heterogeneity is wide (Table~\ref{tab:robust} and \texttt{robustness\_category\_gaps.csv}): fish and seafood (median gap 0.183, 9{,}577 group-weeks, dominated by Metro/Super~C private-label frozen seafood), snacks (0.154) and bakery (0.136) show the largest gaps; fresh produce (0.002) and deli/cheese (0.000) show essentially none---for these categories the matched listings within a chain are priced identically across its banners. H1 is supported on both registered criteria (median gap $\ge 5\%$; AR(1) $\ge 0.5$), with the qualification that most of the evidence is within-chain format differentiation. | |
| 19 | 23 | |
| 20 | −\input{figures/main_figure} | |
| 24 | +\subsection{H2: Promotions double posted-price volatility} | |
| 25 | +Table~\ref{tab:promotions} and Figure~\ref{fig:promotions} summarise Model~B on 102{,}835 storefront listings observed in at least eight weeks. Inferred promotions cover 4.6\% of listing-weeks (4.4\% in the last week, against 9.5\% of active listings carrying an on-sale flag---the flag also captures small reductions and loyalty prices that our 10\% rule excludes; agreement between the two is 90\%). Promotions are deep: the median depth is 0.224 log points (mean 0.29). The weekly change frequency of posted prices is 8.5\%, and \emph{half} of all weekly changes are entries into or exits from a promotion. The standard deviation of weekly log changes is 0.106 for posted prices and 0.055 for the reference series, a ratio of 1.92 (bootstrap 95\% CI 1.84--2.02). The ratio is 2.1--2.8 for the large chains' storefronts (Maxi 2.84, Giant Tiger 2.30, Metro 2.12, Super~C 2.03) and close to one for health-food and online-only shops that rarely promote (La Boîte à Grains 1.01). The registered falsification holds: 4.7\% of listing-weeks display more than one price value in single-store shops against 22.8\% in anti-bot storefronts, so the weekly aggregation was necessary and the store-mixture reading of the oscillation stands. H2 is supported: promotion-adjusted (reference) volatility is about half of raw volatility. | |
| 26 | +\input{tables/tab_promotions} | |
| 21 | 27 | |
| 22 | −The figure illustrates \ldots{} The upward trend in \ldots{} is consistent with the regression estimates reported in Table~\ref{tab:regression}. | |
| 28 | +\begin{figure}[htbp]\centering | |
| 29 | +\includegraphics[width=\textwidth]{figures/fig_promotions.pdf} | |
| 30 | +\caption{Promotion dynamics (Model B, storefront listings). A: event-study coefficients of $\log p$ around the first promoted week of return episodes (listing and week fixed effects, reference week $-1$, 95\% bands clustered by listing). B: depth of uncensored episodes by kind. C: weekly transition matrix between regular and promoted states.}\label{fig:promotions} | |
| 31 | +\end{figure} | |
| 23 | 32 | |
| 24 | −\subsection{Sub-sample Analysis} | |
| 25 | −\label{subsec:subsample} | |
| 33 | +\subsection{H3: Temporary sales return exactly to their reference; no drift within the window} | |
| 34 | +The transition matrix (Figure~\ref{fig:promotions}C) has $P(\text{promo}\to\text{promo})=0.49$ and $P(\text{regular}\to\text{promo})=0.03$. Of 11{,}705 uncensored low-price episodes, 52\% are \emph{return} episodes (the price comes back to within 2\% of the pre-episode reference), 42\% are regular-price resets downward and 6\% resets upward; the 10\% rule, applied to a short panel, flags many permanent price cuts, which is why amendment A-003 was needed. Return episodes---the promotions of the theory---last one week in 80\% of cases (median 1, mean 1.3 weeks) with a median depth of 0.288 log points; the median interval between two episodes of the same listing is three weeks and 23\% of listings have at least two episodes. For return episodes the mean change of the reference price from before to after the sale is $-0.0003$ log points (s.e.\ 0.0001; 0.5\% of references move up, 0.5\% move down), against a placebo of $+0.0003$ (s.e.\ 0.0007) at random dates in never-promoted listings. The point estimate is statistically distinguishable from zero because of the sample size, but it is economically zero: within eleven weeks, Quebec storefront promotions neither precede nor follow a reference-price adjustment. For all low-price episodes the reference falls by 10 log points on average, which is simply the reset-down class. | |
| 26 | 35 | |
| 27 | −To explore potential heterogeneity, we re-estimate Equation~\eqref{eq:baseline} across sub-samples defined by \ldots{} The results, available in Table~\ref{tab:app_subsample} of Appendix~\ref{app:tables}, indicate that \ldots{} | |
| 36 | +The event study (Figure~\ref{fig:promotions}A) shows the price 33 log points below the pre-week during the first promoted week and back to 5--6 points below it afterwards; the pre-period dummies are jointly non-zero ($p<0.001$) and \emph{negative}: the week immediately before a sale is about 5 log points higher than the weeks $-4$ to $-2$ and than the weeks after. Taken at face value this is the ``pre-sale price elevation'' predicted by the reference-price-inflation hypothesis. Splitting by feed type deflates it: in anti-bot storefronts the elevation of week $-1$ relative to weeks $-4\ldots-2$ and $+1\ldots+4$ is 5--9 log points, in single-store shops it is 1--3 log points. The larger part is therefore a store-mixture artefact---the week before an episode is, by construction, a week in which the modal price was the higher value of the store distribution---and the residual two-point elevation in single-store shops is a small, exploratory indication of pre-sale repricing that a longer and store-identified panel should re-examine. We register the direction found for H3 as ``no reference-price drift after temporary sales; a small pre-sale elevation in clean feeds''. | |
| 37 | + | |
| 38 | +Circulars provide the complementary, observed view: 33{,}006 circular items, 22.7\% with an explicit regular price, a median observed depth of 0.224 log points (mean 0.287) that matches the inferred storefront depth, and 60\% of items recurring in more than one weekly circular within the window. | |
| 39 | + | |
| 40 | +\subsection{H4: Small lead--lag associations; cross-company placebo fails} | |
| 41 | +Table~\ref{tab:spillovers} and Figure~\ref{fig:spillovers} report Model~C. A banner's weekly log change is associated with the mean change of its rivals in the same group at lag 0 (0.037, s.e.\ 0.018), lag 1 (0.037, 0.012), lag 2 (0.015, 0.013) and lag 3 (0.043, 0.015), with leads at 0.009 (0.012) and 0.013 (0.015). The probability that a banner changes its price in a week is 25\% if a rival changed its price the week before and 17\% if not. These associations come almost entirely from same-company pairs (lag~1: 0.042, s.e.\ 0.012, on 21{,}192 observations), i.e.\ from Metro following Super~C or Maxi following Provigo on the same UPC---coordination within a chain's pricing calendar rather than competitive response. For other-company rivals the sample is small (1{,}784 observations), the lags are insignificant or negative, and the \emph{lead} coefficient is $-0.108$ (s.e.\ 0.048): rivals' future changes ``predict'' current changes, which a causal response cannot do. The placebo fails, and we draw no causal conclusion. Promotions co-move only weakly: two or more banners of a group are simultaneously at a low in 1.7\% of group-weeks against 0.6\% expected under independence. | |
| 42 | +\input{tables/tab_spillovers} | |
| 43 | + | |
| 44 | +\begin{figure}[htbp]\centering | |
| 45 | +\includegraphics[width=0.8\textwidth]{figures/fig_spillovers.pdf} | |
| 46 | +\caption{Lead--lag associations between a banner's weekly log price change and its rivals' (Model C), 95\% intervals clustered by match group. Shaded region: leads (placebo).}\label{fig:spillovers} | |
| 47 | +\end{figure} | |
| 48 | + | |
| 49 | +\subsection{H5: Composition, not price, drives naive averages} | |
| 50 | +Over eleven weeks storefront posted prices rose by 0.28\% on the chained Jevons matched-model index (bootstrap 95\% CI 0.15--0.42\%), 0.10\% on the fixed-basket Jevons over the 44{,}423 listings present in every week, $-0.05$\% on the fixed-basket Dutot and 2.7\% on the Carli (which is upward-biased by construction); the naive mean of posted prices rose by 0.37\% and the naive median not at all (Table~\ref{tab:indices}, Figure~\ref{fig:indices}). At the market level the registered gap between the naive mean and the matched-model index is 0.06 percentage points with a bootstrap interval of $-0.78$ to $+0.84$: H5 is \emph{not} supported over this window for the market as a whole. It is strongly supported for individual banners whose assortment turned over: the naive mean rose 28.8\% for Walmart, 14.6\% for Maxi and 11.0\% for Provigo while their matched-model indices moved by 0.2\%, 0.9\% and 0.6\%; across all channels the naive mean \emph{fell} 2.9\% because circular items---cheap by construction---entered the catalogue, while the matched-model index rose 0.24\%. Assortment turnover is substantial: of 71{,}694 storefront listings, 20{,}260 entered and 17{,}295 exited during the window (weekly entry rate 3.6\%). Package-size changes within a listing are unobservable in this source. The Statistics Canada index for food purchased from stores in Quebec fell 0.67\% from August to September 2026 while our storefront matched-model index rose 0.10\% over the same two months; with two monthly observations, different coverage (online catalogues vs.\ store collection) and no weights, this is a coherence check, not a validation. | |
| 51 | +\input{tables/tab_indices} | |
| 52 | + | |
| 53 | +\begin{figure}[htbp]\centering | |
| 54 | +\includegraphics[width=0.8\textwidth]{figures/fig_indices.pdf} | |
| 55 | +\caption{Unweighted price indices on storefront listings, 13 August 2026 $=100$: naive mean of posted prices, chained Jevons (matched model), fixed-basket Jevons and Carli. No expenditure weights exist; these are not consumer price indices.}\label{fig:indices} | |
| 56 | +\end{figure} | |
| 57 | + | |
| 58 | +\subsection{H6: Two banners capture most of the posted-price savings} | |
| 59 | +Table~\ref{tab:basket} and Figure~\ref{fig:basket} report Model~E. On the primary block (five banners, 15--18 items depending on the week, weekly mean cost), the best single banner costs 67.3~CAD, two banners 58.5, three 57.2 and the unconstrained optimum 56.9: unconstrained cheapest-banner shopping saves 15.4\% relative to the best single banner, and two banners capture 85\% of that saving. The pattern is stable across block sizes ($B=4$: 15.1\%, 82\%; $B=6$: 11.4\%, 75\%; $B=7$: 12.0\%, 72\%). The spread between the cheapest and the most expensive single banner for the same items is 91\% of the cheapest---banner choice matters far more than multi-banner optimisation. With an illustrative fixed cost of 5~CAD per banner visited the optimum uses 1.8 banners on average, with 10~CAD 1.3 and with 20~CAD one banner only. H6 is supported in its posted-price form; whether the savings are \emph{feasible} depends on store access and travel costs that this source does not observe, so the 15\% is an upper bound on achievable savings from banner choice within the matched catalogues, not an estimate of what a household can realise. | |
| 60 | +\input{tables/tab_basket} | |
| 61 | + | |
| 62 | +\begin{figure}[htbp]\centering | |
| 63 | +\includegraphics[width=\textwidth]{figures/fig_basket.pdf} | |
| 64 | +\caption{Fixed-basket optimisation (Model E, $B=5$). A: mean weekly basket cost against the maximum number of banners visited. B: sensitivity to an illustrative fixed cost per banner visited (travel costs are unobserved).}\label{fig:basket} | |
| 65 | +\end{figure} | |
| 66 | + | |
| 67 | +\subsection{Forecasting} | |
| 68 | +Table~\ref{tab:fprice}, Table~\ref{tab:fpromo} and Figure~\ref{fig:forecast} report Model~F on 215{,}264 test observations over four origins. For next-week log posted price the last value is the best model on mean absolute error (0.0226, against 0.0248 for banner--category drift, 0.0299 ridge and 0.0301 gradient boosting), while ridge and boosting have the lowest root mean squared error (0.099--0.100 against 0.108): the learned models trade frequent small errors for fewer large ones, which is what a mixture of sticky prices and occasional deep promotions implies. For next-week promotion state gradient boosting reaches an AUC of 0.936 and a Brier score of 0.031 against a base rate of 5.7\% (Brier 0.057) and a persistence rule (AUC 0.75, Brier 0.051): promotions are predictable from a listing's own recent history (depth, recurrence, banner, category). These are honest out-of-time results on a short panel; they do not speak to horizons beyond one week. | |
| 69 | +\input{tables/tab_forecast_price}\input{tables/tab_forecast_promo} | |
| 70 | + | |
| 71 | +\begin{figure}[htbp]\centering | |
| 72 | +\includegraphics[width=\textwidth]{figures/fig_forecast.pdf} | |
| 73 | +\caption{Rolling-origin forecast errors by origin week (Model F). A: MAE of next-week log posted price. B: AUC for next-week promotion state.}\label{fig:forecast} | |
| 74 | +\end{figure} | |
| 75 | + | |
| 76 | +\subsection{The QGPAI components} | |
| 77 | +Figure~\ref{fig:qgpai} and Table~\ref{tab:qgpai} present the five components over the eleven weeks. The comparable price index on matched groups dips in late August (98.7 on 27 August) and ends at 100.7 on 22 October, with a bootstrap band over groups of about $\pm$0.5; the promotion share rises from 3--4\% in August to 6--7\% in October while the observed circular depth is flat at 0.21--0.25; the dispersion component is remarkably stable at a median gap of 0.15; the fixed-block basket cost falls from 34 to 31~CAD for the best single banner and from 30 to 28~CAD unconstrained. The fifth component is not computed. Two illustrative composites with prespecified weights are in the replication package; we do not interpret them, because any weighting of these components is a normative choice, not an estimate. | |
| 78 | +\input{tables/tab_qgpai} | |
| 79 | + | |
| 80 | +\begin{figure}[htbp]\centering | |
| 81 | +\includegraphics[width=\textwidth]{figures/fig_qgpai.pdf} | |
| 82 | +\caption{QGPAI components by circular week, 2026. Bands: bootstrap over match groups (components 1 and 3). Component 2 is undefined in the first week (no trailing history). Component 5 (access and affordability) is not computable from this source.}\label{fig:qgpai} | |
| 83 | +\end{figure} | |
modified
paper/sections/robustness.tex
+8 −21
@@ -1,26 +1,13 @@ | ||
| 1 | −% ============================================================================ | |
| 2 | −\section{Robustness Checks} | |
| 3 | −\label{sec:robustness} | |
| 4 | −% ============================================================================ | |
| 1 | +\section{Robustness, falsification and failed tests}\label{sec:robust} | |
| 5 | 2 | |
| 6 | −We conduct several robustness tests to assess the sensitivity of our baseline results. | |
| 3 | +Table~\ref{tab:robust} collects the registered robustness checks on the H1 statistic. The median gap is 0.163 on the balanced panel of 1{,}300 groups present in all eleven weeks, 0.154 with unit-price normalisation, 0.151 after trimming the 1st and 99th percentiles (mean 0.181 against 0.189 untrimmed), 0.183 on cross-company group-weeks only, 0.135 when only non-promoted weeks are kept and 0.237 in group-weeks where at least one banner is promoted; across matching tiers (A, A+B, A+B+C, A+B+C+D) and posted-price definitions (mode, minimum, maximum, time-weighted mean) it ranges from 0.138 to 0.154, and adding circular listings leaves it at 0.154. Dispersion is therefore not an artefact of a tier, a price definition, outliers or promotions, although promotions widen it. | |
| 4 | +\input{tables/tab_robustness} | |
| 7 | 5 | |
| 8 | −\subsection{Alternative Variable Definitions} | |
| 9 | −\label{subsec:alt_definitions} | |
| 6 | +The category-level sign tests that the median gap exceeds 5\% reject for eight of eleven categories after Holm correction and do not reject for fresh produce, deli/cheese and ready meals. Table~\ref{tab:holm} applies Holm's correction across the primary tests: H1 (both criteria), H2, the H3 reference-change tests and the H4 lag-1 association remain significant; the market-level H5 gap does not. | |
| 7 | +\input{tables/tab_holm} | |
| 10 | 8 | |
| 11 | −We first verify that our results are not driven by the specific construction of the dependent variable. Table~\ref{tab:app_robustness} in Appendix~\ref{app:tables} reports estimates using \ldots{} as alternative measures. The coefficient on $X_{i,t}$ remains statistically significant and economically meaningful across all specifications. | |
| 9 | +\paragraph{Falsification tests that passed.} The AR(1) placebo (permuted banner labels) returns 0.004; the H3 placebo at random dates returns $+0.0003$; single-store shops show 4.7\% multi-price weeks against 22.8\% in anti-bot feeds; the H4 leads for all-rival and same-company rivals are indistinguishable from zero. | |
| 12 | 10 | |
| 13 | −\subsection{Alternative Estimation Methods} | |
| 14 | −\label{subsec:alt_estimation} | |
| 11 | +\paragraph{Tests that failed or were inconclusive.} (i) The cross-company lead placebo in Model~C fails ($-0.108$, s.e.\ 0.048): with 1{,}784 observations and common weekly shocks, the descriptive lag coefficients for other-company rivals cannot be read as responses. (ii) The event-study pretrend test rejects; the decomposition by feed type attributes most of the pre-sale elevation to store-mixture noise, leaving a small exploratory residual. (iii) The market-level H5 gap is not different from zero over eleven weeks; the hypothesis holds only at the banner level. (iv) Tier~C matching failed validation (precision 0.45) and was removed from the primary specification; a first version of the matcher based on token-set similarity was rejected outright. (v) The preregistered basket rule left only seven items common to all wide-coverage banners and had to be amended (A-001). (vi) The comparison with Statistics Canada rests on two monthly observations and is inconclusive. We regard reporting these as part of the result. | |
| 15 | 12 | |
| 16 | −We re-estimate the model using \ldots{} to address potential concerns about \ldots{} The results are qualitatively unchanged. | |
| 17 | − | |
| 18 | −\subsection{Placebo Tests} | |
| 19 | −\label{subsec:placebo} | |
| 20 | − | |
| 21 | −As a falsification exercise, we replace the main explanatory variable with \ldots{} If our baseline results reflect a genuine economic relationship rather than spurious correlation, we expect the placebo coefficients to be statistically insignificant. Consistent with this expectation, none of the placebo estimates are significant at conventional levels. | |
| 22 | − | |
| 23 | −\subsection{Sensitivity to Sample Period} | |
| 24 | −\label{subsec:sample_period} | |
| 25 | − | |
| 26 | −We examine whether our results are sensitive to the sample period by \ldots{} The estimates remain stable across sub-periods, suggesting that our findings are not driven by a specific time window. | |
| 13 | +\paragraph{Out-of-time validation.} Model~F's rolling-origin design is the only strictly out-of-time evidence in the paper; the promotion-state forecasts (AUC 0.94) are the component of the architecture that generalises forward within the window. | |
added
paper/sections/theory.tex
+15 −0
@@ -0,0 +1,15 @@ | ||
| 1 | +\section{Theoretical framework}\label{sec:theory} | |
| 2 | + | |
| 3 | +We use a deliberately small model to organise the hypotheses and to be explicit about what posted-price data can identify. Retailer (banner) $r$ sells product $i$ in week $t$ to two consumer types in the spirit of \citet{varian1980} and \citet{salop1977}: a share $1-\lambda_i$ of \emph{loyal} shoppers who buy at their habitual banner if the posted price does not exceed their reservation value $v_i$, and a share $\lambda_i$ of \emph{shoppers} who observe all posted prices (for instance through a comparison service) and buy from the cheapest banner. Banner $r$ faces marginal procurement cost $c_{irt}$, a format-specific cost shifter $\kappa_r$ (service level, store size, location rents) and chooses a posted price $p_{irt}$ and a promotion state $s_{irt}\in\{0,1\}$. Promotions are temporary reductions below a \emph{reference} price $\bar p_{irt}$ that is costly to change (a menu cost $m$ in the sense of \citealp{golosov2007} and \citealp{midrigan2011}) and that loyal shoppers use as their norm. | |
| 4 | + | |
| 5 | +\paragraph{Equilibrium structure.} With $R$ banners carrying the product, the standard result is a mixed-strategy equilibrium in which each banner posts its reference price $\bar p_{ir}=\bar p_{ir}(\kappa_r, c_{ir}, v_i)$ most of the time and randomises temporary reductions to attract shoppers; the probability of a promotion increases in $\lambda_i$ and in the number of rival banners, and the depth of the reduction is bounded by the gap between the reference price and the shopper-competitive price. Three properties are robust across variants of the model and generate our registered hypotheses. | |
| 6 | + | |
| 7 | +\begin{enumerate} | |
| 8 | +\item \textbf{Persistent reference-price dispersion (H1).} Reference prices differ across banners by $\kappa_r$ and $c_{ir}$, which move slowly; hence log gaps between banners on the same product should be large relative to zero and autocorrelated across weeks, whereas a model in which banners price at a common marginal cost predicts gaps near zero and no persistence. Within a company, format differentiation ($\kappa_{\text{discount}}<\kappa_{\text{supermarket}}$) predicts a \emph{stable sign} of the gap (the discount banner is cheaper), which is testable on the Metro/Super~C and Maxi/Provigo pairs. | |
| 9 | +\item \textbf{Sales double the volatility of posted prices without moving the reference (H2, H3).} Because promotions are temporary deviations, the variance of weekly log price changes decomposes into a reference component and a promotion component; the ratio of total to reference volatility exceeds one by the promotion share and depth. The model with a menu cost on $\bar p$ predicts that the reference price after a sale equals the reference before it (no drift). The alternative ``reference-price inflation'' hypothesis---the retailer raises $\bar p$ before or after a sale so that the displayed discount looks larger---predicts a pre-sale elevation or a post-sale increase of the reference. | |
| 10 | +\item \textbf{Competitive spillovers are an equilibrium feature, not a causal response (H4).} In the mixed-strategy equilibrium promotions are independent draws conditional on the state; observed co-movement of prices across banners arises from common cost shocks $c_{it}$ and common calendar events (circular weeks), so lead--lag regressions of one banner's changes on another's identify a response only if a rival's change is exogenous. We have no such instrument; the model therefore tells us to treat contemporaneous and lagged associations as descriptive and to use \emph{leads} as a placebo: a response cannot precede its cause. | |
| 11 | +\end{enumerate} | |
| 12 | + | |
| 13 | +\paragraph{Measured inflation and the basket (H5, H6).} If the set of listings changes over time (entry of expensive items, exit of cheap ones), an average of posted prices moves even when every continuing item's price is unchanged; a matched-model index conditions on continuing items and recovers the price change of the fixed set. The difference is an identified object (the composition effect) that does not require quantities. The shopper's problem is a set-covering problem: choose at most $K$ banners and an assignment of items to banners to minimise the cost of a fixed basket; the model predicts decreasing returns to $K$ because the cheapest banner for one item is often the cheapest for others (a low $\kappa_r$ shifts all prices). | |
| 14 | + | |
| 15 | +\paragraph{What is not identified.} $\lambda_i$, $v_i$, $m$ and $c_{irt}$ are not identified from posted prices alone; neither are store-level $\kappa$ within a banner, since stores are unobserved---although the store-mixture oscillation of Section~\ref{sec:mixture} reveals that within-banner store dispersion exists and bounds its size. We make no attempt to estimate these primitives. | |
modified
paper/tables/tab_coverage_feeds.tex
+29 −29
@@ -1,39 +1,39 @@ | ||
| 1 | 1 | \begin{table}[htbp]\centering |
| 2 | −\begin{threeparttable} | |
| 3 | 2 | \caption{Coverage of the 24 largest source feeds (snapshot of 1 November 2026)}\label{tab:coverage} |
| 4 | 3 | \small |
| 4 | +\begin{adjustbox}{max width=\textwidth} | |
| 5 | 5 | \begin{tabular}{lrrrrrrrr} |
| 6 | 6 | \toprule |
| 7 | 7 | Company & Banner & Channel & Listings & Active & Median days & Price events & Unit price & Brand \\ |
| 8 | 8 | \midrule |
| 9 | −Metro Inc. & Metro & online\_storefront & 16\,382 & 1\,540 & 50.00 & 63868.00 & 0.91 & 0.88 \\ | |
| 10 | −Giant Tiger Stores & Giant Tiger & online\_storefront & 10\,352 & 9\,617 & 81.00 & 16810.00 & 0.74 & 1.00 \\ | |
| 11 | −Société des alcools du Québec & SAQ & online\_storefront & 7\,477 & 3\,293 & 36.00 & 8153.00 & 1.00 & 1.00 \\ | |
| 12 | −Metro Inc. & Super C & online\_storefront & 6\,418 & 857 & 48.00 & 15210.00 & 0.94 & 0.93 \\ | |
| 13 | −La Boîte à Grains & La Boîte à Grains & online\_storefront & 6\,279 & 6\,000 & 75.00 & 11903.00 & 0.76 & 1.00 \\ | |
| 14 | −Marché Nuvo & Marché Nuvo & online\_storefront & 6\,224 & 4\,970 & 75.00 & 8186.00 & 0.97 & 1.00 \\ | |
| 15 | −Epipresto & Epipresto & online\_storefront & 6\,000 & 6\,000 & 75.00 & 5984.00 & 0.32 & 1.00 \\ | |
| 16 | −Walmart Canada & Walmart & online\_storefront & 4\,601 & 922 & 19.00 & 7872.00 & 0.56 & 0.28 \\ | |
| 17 | −Supermarché PA & PA Supermarché & online\_storefront & 3\,602 & 3\,554 & 81.00 & 6864.00 & 0.19 & 1.00 \\ | |
| 18 | −Walmart Canada & Walmart & circular & 3\,330 & 219 & 8.00 & 3501.00 & 0.09 & 0.66 \\ | |
| 19 | −Sobeys / Empire & IGA & online\_storefront & 3\,083 & 600 & 34.00 & 5848.00 & 0.95 & 0.94 \\ | |
| 20 | −Metro Inc. & Marché Adonis & online\_storefront & 2\,950 & 447 & 52.00 & 5935.00 & 0.93 & 0.00 \\ | |
| 21 | −Sobeys / Empire & IGA & circular & 2\,933 & 350 & 9.00 & 3527.00 & 0.03 & 0.75 \\ | |
| 22 | −Aliments Merci & Aliments Merci & online\_storefront & 2\,859 & 2\,838 & 75.00 & 3367.00 & 0.05 & 0.85 \\ | |
| 23 | −Loblaw Companies & Club Entrepôt & online\_storefront & 2\,808 & 763 & 74.00 & 68438.00 & 1.00 & 0.76 \\ | |
| 24 | −Loblaw Companies & Maxi & online\_storefront & 2\,441 & 1\,086 & 78.00 & 13674.00 & 1.00 & 0.82 \\ | |
| 25 | −Metro Inc. & Metro & circular & 2\,256 & 314 & 9.00 & 2871.00 & 0.01 & 0.00 \\ | |
| 26 | −Loblaw Companies & Provigo & online\_storefront & 2\,138 & 450 & 77.00 & 5582.00 & 1.00 & 0.80 \\ | |
| 27 | −Loblaw Companies & Maxi & circular & 2\,107 & 271 & 9.00 & 2341.00 & 0.62 & 0.85 \\ | |
| 28 | −Metro Inc. & Super C & circular & 1\,954 & 192 & 9.00 & 2448.00 & 0.00 & 0.30 \\ | |
| 29 | −Loblaw Companies & Provigo & circular & 1\,914 & 175 & 9.00 & 2035.00 & 0.55 & 0.79 \\ | |
| 30 | −Metro Inc. & Marché Adonis & circular & 1\,882 & 203 & 9.00 & 2131.00 & 0.01 & 0.62 \\ | |
| 31 | −Pasquier & Pasquier & circular & 1\,611 & 152 & 9.00 & 1727.00 & 0.21 & 0.75 \\ | |
| 32 | −Loblaw Companies & Pharmaprix & circular & 1\,225 & 167 & 8.00 & 1378.00 & 0.51 & 0.51 \\ | |
| 9 | +Metro Inc. & Metro & online\_storefront & 16\,382 & 1\,540 & 50 & 63\,868 & 0.91 & 0.88 \\ | |
| 10 | +Giant Tiger Stores & Giant Tiger & online\_storefront & 10\,352 & 9\,617 & 81 & 16\,810 & 0.74 & 1.00 \\ | |
| 11 | +Société des alcools du Québec & SAQ & online\_storefront & 7\,477 & 3\,293 & 36 & 8\,153 & 1.00 & 1.00 \\ | |
| 12 | +Metro Inc. & Super C & online\_storefront & 6\,418 & 857 & 48 & 15\,210 & 0.94 & 0.93 \\ | |
| 13 | +La Boîte à Grains & La Boîte à Grains & online\_storefront & 6\,279 & 6\,000 & 75 & 11\,903 & 0.76 & 1.00 \\ | |
| 14 | +Marché Nuvo & Marché Nuvo & online\_storefront & 6\,224 & 4\,970 & 75 & 8\,186 & 0.97 & 1.00 \\ | |
| 15 | +Epipresto & Epipresto & online\_storefront & 6\,000 & 6\,000 & 75 & 5\,984 & 0.32 & 1.00 \\ | |
| 16 | +Walmart Canada & Walmart & online\_storefront & 4\,601 & 922 & 19 & 7\,872 & 0.56 & 0.28 \\ | |
| 17 | +Supermarché PA & PA Supermarché & online\_storefront & 3\,602 & 3\,554 & 81 & 6\,864 & 0.19 & 1.00 \\ | |
| 18 | +Walmart Canada & Walmart & circular & 3\,330 & 219 & 8 & 3\,501 & 0.09 & 0.66 \\ | |
| 19 | +Sobeys / Empire & IGA & online\_storefront & 3\,083 & 600 & 34 & 5\,848 & 0.95 & 0.94 \\ | |
| 20 | +Metro Inc. & Marché Adonis & online\_storefront & 2\,950 & 447 & 52 & 5\,935 & 0.93 & 0.00 \\ | |
| 21 | +Sobeys / Empire & IGA & circular & 2\,933 & 350 & 9 & 3\,527 & 0.03 & 0.75 \\ | |
| 22 | +Aliments Merci & Aliments Merci & online\_storefront & 2\,859 & 2\,838 & 75 & 3\,367 & 0.05 & 0.85 \\ | |
| 23 | +Loblaw Companies & Club Entrepôt & online\_storefront & 2\,808 & 763 & 74 & 68\,438 & 1.00 & 0.76 \\ | |
| 24 | +Loblaw Companies & Maxi & online\_storefront & 2\,441 & 1\,086 & 78 & 13\,674 & 1.00 & 0.82 \\ | |
| 25 | +Metro Inc. & Metro & circular & 2\,256 & 314 & 9 & 2\,871 & 0.01 & 0.00 \\ | |
| 26 | +Loblaw Companies & Provigo & online\_storefront & 2\,138 & 450 & 77 & 5\,582 & 1.00 & 0.80 \\ | |
| 27 | +Loblaw Companies & Maxi & circular & 2\,107 & 271 & 9 & 2\,341 & 0.62 & 0.85 \\ | |
| 28 | +Metro Inc. & Super C & circular & 1\,954 & 192 & 9 & 2\,448 & 0.00 & 0.30 \\ | |
| 29 | +Loblaw Companies & Provigo & circular & 1\,914 & 175 & 9 & 2\,035 & 0.55 & 0.79 \\ | |
| 30 | +Metro Inc. & Marché Adonis & circular & 1\,882 & 203 & 9 & 2\,131 & 0.01 & 0.62 \\ | |
| 31 | +Pasquier & Pasquier & circular & 1\,611 & 152 & 9 & 1\,727 & 0.21 & 0.75 \\ | |
| 32 | +Loblaw Companies & Pharmaprix & circular & 1\,225 & 167 & 8 & 1\,378 & 0.51 & 0.51 \\ | |
| 33 | 33 | \bottomrule |
| 34 | 34 | \end{tabular} |
| 35 | −\begin{tablenotes}[flushleft]\footnotesize | |
| 36 | −\item Listings = all uids ever observed in the feed (active and deactivated); Active = listed on 1 Nov 2026; Median days = median observation span; Price events = rows in the price log; Unit price / Brand = share of active listings with a parsable package size (re-parsed) / a brand field. Channel: online storefront vs weekly circular (Flipp, postal code H2X 1Y4). Full table of 54 feeds in the replication package. | |
| 37 | −\end{tablenotes} | |
| 38 | −\end{threeparttable} | |
| 35 | +\end{adjustbox} | |
| 36 | +\begin{minipage}{\textwidth}\footnotesize\vspace{2pt} | |
| 37 | +Listings = all uids ever observed in the feed (active and deactivated); Active = listed on 1 Nov 2026; Median days = median observation span; Price events = rows in the price log; Unit price / Brand = share of active listings with a parsable package size (re-parsed) / a brand field. Channel: online storefront vs weekly circular (Flipp, postal code H2X 1Y4). Full table of 54 feeds in the replication package. | |
| 38 | +\end{minipage} | |
| 39 | 39 | \end{table} |
modified
paper/tables/tab_indices.tex
+5 −5
@@ -1,7 +1,7 @@ | ||
| 1 | 1 | \begin{table}[htbp]\centering |
| 2 | −\begin{threeparttable} | |
| 3 | 2 | \caption{Cumulative price change over 11 weeks (13 Aug $\to$ 22 Oct 2026) under alternative unweighted index formulas}\label{tab:indices} |
| 4 | 3 | \small |
| 4 | +\begin{adjustbox}{max width=\textwidth} | |
| 5 | 5 | \begin{tabular}{lrrrrrrr} |
| 6 | 6 | \toprule |
| 7 | 7 | Sample & Naive mean & Naive median & Chained Jevons & Fixed Jevons & Fixed Dutot & Fixed Carli & Balanced n \\ |
@@ -11,8 +11,8 @@ Storefront, unit price & -0.012 & -0.007 & 0.003 & 0.001 & -0.004 & 0.036 & 29\, | ||
| 11 | 11 | All channels, posted price & -0.029 & 0.000 & 0.002 & 0.001 & -0.001 & 0.028 & 45\,214 \\ |
| 12 | 12 | \bottomrule |
| 13 | 13 | \end{tabular} |
| 14 | −\begin{tablenotes}[flushleft]\footnotesize | |
| 15 | −\item No expenditure weights exist: none of these is a consumer price index. Bootstrap (999 listing resamples) 95\% CI for the storefront chained Jevons: [0.002, 0.004]; naive-mean minus Jevons gap: 0.001 [-0.008, 0.008]. Statistics Canada CPI, Quebec, food purchased from stores: Aug$\to$Sep 2026 -0.007 (our chained Jevons, monthly means, +0.001); 12-month CPI change to Sep 2026 +0.016. | |
| 16 | −\end{tablenotes} | |
| 17 | −\end{threeparttable} | |
| 14 | +\end{adjustbox} | |
| 15 | +\begin{minipage}{\textwidth}\footnotesize\vspace{2pt} | |
| 16 | +No expenditure weights exist: none of these is a consumer price index. Bootstrap (999 listing resamples) 95\% CI for the storefront chained Jevons: [0.002, 0.004]; naive-mean minus Jevons gap: 0.001 [-0.008, 0.008]. Statistics Canada CPI, Quebec, food purchased from stores: Aug$\to$Sep 2026 -0.007 (our chained Jevons, monthly means, +0.001); 12-month CPI change to Sep 2026 +0.016. | |
| 17 | +\end{minipage} | |
| 18 | 18 | \end{table} |
modified
paper/tables/tab_match_validation.tex
+5 −5
@@ -1,7 +1,7 @@ | ||
| 1 | 1 | \begin{table}[htbp]\centering |
| 2 | −\begin{threeparttable} | |
| 3 | 2 | \caption{Matching validation: stratified sample (60 pairs per tier) and UPC ground truth}\label{tab:validation} |
| 4 | 3 | \small |
| 4 | +\begin{adjustbox}{max width=\textwidth} | |
| 5 | 5 | \begin{tabular}{lrrrrrrrrr} |
| 6 | 6 | \toprule |
| 7 | 7 | Tier & n & Same & Different & Unsure & Precision & CI low & CI high & UPC precision & UPC recall \\ |
@@ -12,8 +12,8 @@ C & 60 & 27 & 30 & 3 & 0.450 & 0.331 & 0.575 & 0.845 & 0.989 \\ | ||
| 12 | 12 | D & 60 & 59 & 0 & 1 & 0.983 & 0.911 & 0.997 & & \\ |
| 13 | 13 | \bottomrule |
| 14 | 14 | \end{tabular} |
| 15 | −\begin{tablenotes}[flushleft]\footnotesize | |
| 16 | −\item Precision = share labelled SAME (Wilson 95\% CI); labels by Claude (claude-opus-5-5) as second annotator with full operator review of every pair; PI adjudication pending. UPC columns: precision/recall of the tier's text rule on all Metro\,$\times$\,Super~C candidate pairs within brand\,$\times$\,package blocks, where the shared GS1 code is the truth (B: identical token sets; C: token-sort ratio $\ge 0.90$). Tier D has no UPC benchmark. | |
| 17 | −\end{tablenotes} | |
| 18 | −\end{threeparttable} | |
| 15 | +\end{adjustbox} | |
| 16 | +\begin{minipage}{\textwidth}\footnotesize\vspace{2pt} | |
| 17 | +Precision = share labelled SAME (Wilson 95\% CI); labels by Claude (claude-opus-5-5) as second annotator with full operator review of every pair; PI adjudication pending. UPC columns: precision/recall of the tier's text rule on all Metro\,$\times$\,Super~C candidate pairs within brand\,$\times$\,package blocks, where the shared GS1 code is the truth (B: identical token sets; C: token-sort ratio $\ge 0.90$). Tier D has no UPC benchmark. | |
| 18 | +\end{minipage} | |
| 19 | 19 | \end{table} |
modified
paper/tables/tab_qgpai.tex
+5 −5
@@ -1,7 +1,7 @@ | ||
| 1 | 1 | \begin{table}[htbp]\centering |
| 2 | −\begin{threeparttable} | |
| 3 | 2 | \caption{QGPAI components by circular week (Thursday start), 2026}\label{tab:qgpai} |
| 4 | 3 | \small |
| 4 | +\begin{adjustbox}{max width=\textwidth} | |
| 5 | 5 | \begin{tabular}{lrrrrrrrrr} |
| 6 | 6 | \toprule |
| 7 | 7 | Week & Comparable index & CI low & CI high & Promo share & Dispersion & Basket K=1 & Basket K=2 & Basket uncon. & Circular depth \\ |
@@ -19,8 +19,8 @@ Week & Comparable index & CI low & CI high & Promo share & Dispersion & Basket K | ||
| 19 | 19 | 22 Oct & 100.688 & 100.106 & 101.212 & 0.054 & 0.151 & 31.165 & 27.907 & 27.907 & 0.229 \\ |
| 20 | 20 | \bottomrule |
| 21 | 21 | \end{tabular} |
| 22 | −\begin{tablenotes}[flushleft]\footnotesize | |
| 23 | −\item Component 5 (access and affordability) is not computed: no store geography or income data are joined. Comparable index: chained Jevons over matched groups (A+B), base 13 Aug = 100, bootstrap band over groups; Dispersion: median within-group max$-$min log gap; Basket: Model E (B = 5). Composites are illustrative only and reported in the replication package. | |
| 24 | −\end{tablenotes} | |
| 25 | −\end{threeparttable} | |
| 22 | +\end{adjustbox} | |
| 23 | +\begin{minipage}{\textwidth}\footnotesize\vspace{2pt} | |
| 24 | +Component 5 (access and affordability) is not computed: no store geography or income data are joined. Comparable index: chained Jevons over matched groups (A+B), base 13 Aug = 100, bootstrap band over groups; Dispersion: median within-group max$-$min log gap; Basket: Model E (B = 5). Composites are illustrative only and reported in the replication package. | |
| 25 | +\end{minipage} | |
| 26 | 26 | \end{table} |
modified
paper/tables/tab_spillovers.tex
+5 −5
@@ -1,7 +1,7 @@ | ||
| 1 | 1 | \begin{table}[htbp]\centering |
| 2 | −\begin{threeparttable} | |
| 3 | 2 | \caption{Descriptive lead--lag associations between a banner's weekly log price change and rivals' changes (Model C)}\label{tab:spillovers} |
| 4 | 3 | \small |
| 4 | +\begin{adjustbox}{max width=\textwidth} | |
| 5 | 5 | \begin{tabular}{lrrrrrrr} |
| 6 | 6 | \toprule |
| 7 | 7 | Rival set & N & $k=0$ & $k=1$ & $k=2$ & $k=3$ & lead 1 & lead 2 \\ |
@@ -11,8 +11,8 @@ Same-company rivals & 21\,192 & 0.028 (0.018) & 0.042 (0.012) & 0.011 (0.013) & | ||
| 11 | 11 | Other-company rivals & 1\,784 & 0.081 (0.066) & -0.001 (0.048) & -0.057 (0.029) & 0.047 (0.060) & -0.108 (0.048) & -0.064 (0.049) \\ |
| 12 | 12 | \bottomrule |
| 13 | 13 | \end{tabular} |
| 14 | −\begin{tablenotes}[flushleft]\footnotesize | |
| 15 | −\item Dependent variable: $\Delta\log p$ of banner $r$ for product $i$ in week $w$; regressors: mean $\Delta\log p$ of the other banners in the match group at $w-k$ (lags) and $w+k$ (leads, placebo). Banner and week fixed effects; s.e. clustered by match group. Contemporaneous terms absorb common shocks and are not causal; a non-zero lead coefficient indicates timing noise or anticipation and is reported as a failed placebo. | |
| 16 | −\end{tablenotes} | |
| 17 | −\end{threeparttable} | |
| 14 | +\end{adjustbox} | |
| 15 | +\begin{minipage}{\textwidth}\footnotesize\vspace{2pt} | |
| 16 | +Dependent variable: $\Delta\log p$ of banner $r$ for product $i$ in week $w$; regressors: mean $\Delta\log p$ of the other banners in the match group at $w-k$ (lags) and $w+k$ (leads, placebo). Banner and week fixed effects; s.e. clustered by match group. Contemporaneous terms absorb common shocks and are not causal; a non-zero lead coefficient indicates timing noise or anticipation and is reported as a failed placebo. | |
| 17 | +\end{minipage} | |
| 18 | 18 | \end{table} |
added
paper/uq_logo.jpg
+0 −0
Binary file not shown.
added
src/literature/make_bib.py
+37 −0
@@ -0,0 +1,37 @@ | ||
| 1 | +"""Build paper/references.bib from literature/references_verified.csv (only verified entries).""" | |
| 2 | +import re, pandas as pd | |
| 3 | +from src.common import ROOT, get_logger | |
| 4 | +log = get_logger("bib") | |
| 5 | + | |
| 6 | +def tex(s): return str(s).replace("&", "&").replace("\\&", "&").replace("&", "\\&") if isinstance(s, str) else "" | |
| 7 | + | |
| 8 | +def main(): | |
| 9 | + df = pd.read_csv(ROOT / "literature" / "references_verified.csv"); df = df[df.verified == True] | |
| 10 | + out = [] | |
| 11 | + for _, r in df.iterrows(): | |
| 12 | + title = tex(r.crossref_title if isinstance(r.crossref_title, str) and r.crossref_title else r.title).replace("‐", "-") | |
| 13 | + authors = r.authors if isinstance(r.authors, str) and r.authors else r.author | |
| 14 | + authors = " and ".join(a.strip() for a in str(authors).split(";") if a.strip()) if ";" in str(authors) else str(authors) | |
| 15 | + yr = int(r.pub_year) if pd.notna(r.pub_year) else int(r.year) | |
| 16 | + typ = str(r.type) if isinstance(r.type, str) else "journal-article" | |
| 17 | + if typ == "report" or r.key == "competitionbureau2023": | |
| 18 | + out.append(f"@techreport{{{r.key},\n author = {{{authors}}},\n title = {{{title}}},\n institution = {{Competition Bureau Canada}},\n year = {{{yr}}},\n url = {{https://competition-bureau.canada.ca}}\n}}\n") | |
| 19 | + continue | |
| 20 | + if typ == "posted-content": # preprint / working paper (SSRN): cite as unpublished work with DOI | |
| 21 | + out.append(f"@unpublished{{{r.key},\n author = {{{authors}}},\n title = {{{title}}},\n note = {{Working paper, SSRN}},\n year = {{{yr}}},\n doi = {{{r.doi}}}\n}}\n") | |
| 22 | + continue | |
| 23 | + if typ in ("book", "monograph"): | |
| 24 | + out.append(f"@book{{{r.key},\n author = {{{authors}}},\n title = {{{title}}},\n publisher = {{{tex(r.publisher)}}},\n year = {{{yr}}}" + (f",\n doi = {{{r.doi}}}" if isinstance(r.doi, str) else "") + "\n}\n") | |
| 25 | + continue | |
| 26 | + fields = [f" author = {{{authors}}}", f" title = {{{title}}}", f" journal = {{{tex(r.journal)}}}", f" year = {{{yr}}}"] | |
| 27 | + if pd.notna(r.volume) and str(r.volume) != "nan": fields.append(f" volume = {{{str(r.volume).replace('.0', '')}}}") | |
| 28 | + if pd.notna(r.issue) and str(r.issue) != "nan": fields.append(f" number = {{{str(r.issue).replace('.0', '')}}}") | |
| 29 | + if pd.notna(r.pages) and str(r.pages) != "nan": fields.append(f" pages = {{{str(r.pages).replace('-', '--')}}}") | |
| 30 | + if isinstance(r.doi, str) and r.doi: fields.append(f" doi = {{{r.doi}}}") | |
| 31 | + elif isinstance(r.note, str) and "jstor" in r.note: fields.append(f" note = {{{re.search(r'https?://[^ ]+', r.note).group(0)}}}") | |
| 32 | + out.append(f"@article{{{r.key},\n" + ",\n".join(fields) + "\n}\n") | |
| 33 | + (ROOT / "paper" / "references.bib").write_text("% Generated from literature/references_verified.csv — every entry verified via Crossref or a stable URL\n" + "\n".join(out)) | |
| 34 | + log.info("wrote %d entries", len(out)) | |
| 35 | + | |
| 36 | +if __name__ == "__main__": | |
| 37 | + main() | |
modified
src/reports/make_figures.py
+15 −14
@@ -3,13 +3,14 @@ | ||
| 3 | 3 | import json, numpy as np, pandas as pd, matplotlib |
| 4 | 4 | matplotlib.use("Agg") |
| 5 | 5 | import matplotlib.pyplot as plt |
| 6 | +import matplotlib.dates as mdates | |
| 6 | 7 | from matplotlib.colors import LinearSegmentedColormap |
| 7 | 8 | from src.common import ROOT, path, get_logger |
| 8 | 9 | log = get_logger("figures"); P = path("processed"); F = ROOT / "paper" / "figures"; E = ROOT / "experiments"; F.mkdir(exist_ok=True) |
| 9 | 10 | C = ["#2a78d6", "#eb6834", "#1baf7a", "#eda100", "#e87ba4", "#008300", "#6250d6", "#e34948"] |
| 10 | 11 | SEQ = LinearSegmentedColormap.from_list("seq", ["#cde2fb", "#86b6ef", "#3987e5", "#256abf", "#184f95", "#0d366b"]) |
| 11 | 12 | TXT, TXT2, GRID = "#0b0b0b", "#52514e", "#e6e5e1" |
| 12 | −plt.rcParams.update({"font.size": 9, "axes.edgecolor": TXT2, "axes.labelcolor": TXT, "xtick.color": TXT2, "ytick.color": TXT2, "axes.spines.top": False, "axes.spines.right": False, | |
| 13 | +plt.rcParams.update({"figure.constrained_layout.use": True, "font.size": 8.5, "axes.titlesize": 9, "axes.edgecolor": TXT2, "axes.labelcolor": TXT, "xtick.color": TXT2, "ytick.color": TXT2, "axes.spines.top": False, "axes.spines.right": False, | |
| 13 | 14 | "axes.grid": True, "grid.color": GRID, "grid.linewidth": 0.6, "axes.axisbelow": True, "legend.frameon": False, "pdf.fonttype": 42}) |
| 14 | 15 | |
| 15 | 16 | def save(fig, name): |
@@ -20,7 +21,7 @@ def fig_coverage(): | ||
| 20 | 21 | fig, axes = plt.subplots(1, 2, figsize=(7.2, 3.0), gridspec_kw={"width_ratios": [1.3, 1]}) |
| 21 | 22 | ax = axes[0]; ax.plot(wk.week, wk.online, color=C[0], lw=2, marker="o", ms=4); ax.plot(wk.week, wk.circular, color=C[1], lw=2, marker="o", ms=4) |
| 22 | 23 | ax.text(wk.week.iloc[-1], wk.online.iloc[-1], " storefront", color=TXT, va="center"); ax.text(wk.week.iloc[-1], wk.circular.iloc[-1], " circular", color=TXT, va="center") |
| 23 | − ax.set_ylabel("Listings observed in week"); ax.set_title("A. Weekly coverage by channel", loc="left", color=TXT); ax.tick_params(axis="x", rotation=30); ax.set_ylim(0) | |
| 24 | + ax.set_ylabel("Listings observed in week"); ax.set_title("A. Weekly coverage by channel", loc="left", color=TXT); ax.tick_params(axis="x", rotation=30); ax.set_ylim(0); ax.xaxis.set_major_formatter(mdates.DateFormatter("%d %b")); ax.set_xlim(right=wk.week.iloc[-1] + pd.Timedelta(days=20)) | |
| 24 | 25 | ax = axes[1]; comp = feed.groupby("company").active.sum().sort_values(ascending=True).tail(10) |
| 25 | 26 | ax.barh(comp.index, comp.values, color=C[0], height=0.6); ax.set_title("B. Active listings by company (top 10)", loc="left", color=TXT); ax.set_xlabel("Active listings, 1 Nov 2026") |
| 26 | 27 | for i, v in enumerate(comp.values): ax.text(v, i, f" {v:,}", va="center", color=TXT2, fontsize=8) |
@@ -35,7 +36,7 @@ def fig_attrition(): | ||
| 35 | 36 | ax.set_yticks(y); ax.set_yticklabels(labels); ax.set_xlabel("Listings"); ax.set_title("A. Attrition flow", loc="left", color=TXT); ax.grid(axis="y", visible=False) |
| 36 | 37 | for yi, n in zip(y, att.listings): ax.text(n, yi, f" {n:,}", va="center", color=TXT2, fontsize=8) |
| 37 | 38 | ax = axes[1]; x = np.arange(4); ax.bar(x - 0.2, v.precision_strict, 0.38, color=C[0], label="Text review (240 pairs)"); ax.bar(x + 0.2, v.upc_precision.fillna(0), 0.38, color=C[1], label="UPC ground truth") |
| 38 | − ax.errorbar(x - 0.2, v.precision_strict, yerr=[v.precision_strict - v.ci_lo, v.ci_hi - v.precision_strict], fmt="none", ecolor=TXT2, lw=1) | |
| 39 | + ax.errorbar(x - 0.2, v.precision_strict, yerr=[np.maximum(0, v.precision_strict - v.ci_lo), np.maximum(0, v.ci_hi - v.precision_strict)], fmt="none", ecolor=TXT2, lw=1) | |
| 39 | 40 | ax.set_xticks(x); ax.set_xticklabels([f"Tier {t}" for t in v.tier]); ax.set_ylim(0, 1.05); ax.set_ylabel("Precision"); ax.set_title("B. Matching precision by tier", loc="left", color=TXT); ax.legend(loc="lower left", fontsize=7); ax.grid(axis="x", visible=False) |
| 40 | 41 | save(fig, "fig_matching") |
| 41 | 42 | |
@@ -59,7 +60,7 @@ def fig_heatmap(): | ||
| 59 | 60 | for j in range(len(banners)): |
| 60 | 61 | if not np.isnan(M.values[i, j]): ax.text(j, i, f"{M.values[i, j]:+.2f}", ha="center", va="center", fontsize=6.5, color=TXT) |
| 61 | 62 | cb = fig.colorbar(im, ax=ax, fraction=0.04); cb.set_label("Median log price gap, row banner minus column banner") |
| 62 | − ax.set_title("Pairwise median gaps on identical products (pairs with ≥ 30 group-weeks)", loc="left", color=TXT, fontsize=9) | |
| 63 | + ax.set_title("Median log gap, row minus column (pairs ≥ 30 group-weeks)", loc="left", color=TXT, fontsize=9) | |
| 63 | 64 | save(fig, "fig_heatmap") |
| 64 | 65 | |
| 65 | 66 | def fig_promotions(): |
@@ -67,7 +68,7 @@ def fig_promotions(): | ||
| 67 | 68 | ep = pd.read_parquet(P / "modelB_episodes.parquet"); tm = pd.read_csv(P / "modelB_transition_matrix.csv", index_col=0) |
| 68 | 69 | fig, axes = plt.subplots(1, 3, figsize=(7.6, 2.8), gridspec_kw={"width_ratios": [1.3, 1, 0.8]}) |
| 69 | 70 | ax = axes[0]; ax.fill_between(es.k, es.coef - 1.96 * es.se, es.coef + 1.96 * es.se, color=C[0], alpha=0.18, lw=0); ax.plot(es.k, es.coef, color=C[0], lw=2, marker="o", ms=4); ax.axhline(0, color=TXT2, lw=0.8); ax.axvline(-0.5, color=TXT2, lw=0.8, ls=":") |
| 70 | − ax.set_xlabel("Weeks relative to first promoted week"); ax.set_ylabel("Log price vs week −1"); ax.set_title("A. Price path around temporary sales", loc="left", color=TXT) | |
| 71 | + ax.set_xlabel("Weeks relative to first promoted week"); ax.set_ylabel("Log price vs week −1"); ax.set_title("A. Price path, temporary sales", loc="left", color=TXT) | |
| 71 | 72 | ax = axes[1]; unc = ep[~ep.censored]; ax.hist(unc[unc.kind == "return"].depth, bins=np.linspace(0, 1.2, 31), color=C[0], alpha=0.85, label="return (temporary)"); ax.hist(unc[unc.kind == "reset_down"].depth, bins=np.linspace(0, 1.2, 31), color=C[1], alpha=0.6, label="reset down") |
| 72 | 73 | ax.set_xlabel("Episode depth, log(reference / price)"); ax.set_ylabel("Episodes"); ax.legend(fontsize=7); ax.set_title("B. Depth by episode kind", loc="left", color=TXT); ax.grid(axis="x", visible=False) |
| 73 | 74 | ax = axes[2]; im = ax.imshow(tm.values, cmap=SEQ, vmin=0, vmax=1); ax.set_xticks([0, 1]); ax.set_xticklabels(["regular", "promo"]); ax.set_yticks([0, 1]); ax.set_yticklabels(["regular", "promo"]); ax.set_xlabel("state in week w"); ax.set_ylabel("state in week w−1"); ax.grid(False) |
@@ -84,7 +85,7 @@ def fig_spillovers(): | ||
| 84 | 85 | i = order[col]; vals = [g[g.term == col + s].coef.iloc[0] for s in xs]; ses = [g[g.term == col + s].se.iloc[0] for s in xs] |
| 85 | 86 | ax.errorbar(np.arange(6) + (i - 1) * 0.22, vals, yerr=[1.96 * v for v in ses], fmt="o", color=C[i], ms=4, lw=1, label=lab[col]) |
| 86 | 87 | ax.axhline(0, color=TXT2, lw=0.8); ax.axvspan(-0.5, 1.5, color=GRID, alpha=0.5, lw=0); ax.set_xticks(range(6)); ax.set_xticklabels(xl); ax.set_ylabel("Response of own Δlog p"); ax.legend(fontsize=7, ncol=3, loc="upper left") |
| 87 | − ax.set_title("Lead–lag association with rivals' price changes (shaded = placebo leads)", loc="left", color=TXT, fontsize=9); ax.grid(axis="x", visible=False) | |
| 88 | + ax.set_title("Own Δlog p on rivals' Δlog p (shaded = placebo leads)", loc="left", color=TXT, fontsize=9); ax.grid(axis="x", visible=False) | |
| 88 | 89 | save(fig, "fig_spillovers") |
| 89 | 90 | |
| 90 | 91 | def fig_indices(): |
@@ -93,21 +94,21 @@ def fig_indices(): | ||
| 93 | 94 | ax.plot(s.week, s.index_mean, color=C[1], lw=2, marker="o", ms=3); ax.plot(s.week, s["index"], color=C[0], lw=2, marker="o", ms=3); ax.plot(s.week, s.jevons, color=C[2], lw=2, marker="o", ms=3, ls="--"); ax.plot(s.week, s.carli, color=C[3], lw=1.5, marker="o", ms=3, ls=":") |
| 94 | 95 | for col, name in [("index_mean", "naive mean of posted prices"), ("index", "chained Jevons (matched model)"), ("jevons", "fixed-basket Jevons"), ("carli", "fixed-basket Carli")]: |
| 95 | 96 | ax.text(s.week.iloc[-1], s[col].iloc[-1], " " + name, va="center", fontsize=7.5, color=TXT) |
| 96 | − ax.set_ylabel("Index (13 Aug 2026 = 100)"); ax.tick_params(axis="x", rotation=30); ax.set_title("Unweighted price indices, storefront listings", loc="left", color=TXT, fontsize=9) | |
| 97 | + ax.set_ylabel("Index (13 Aug 2026 = 100)"); ax.tick_params(axis="x", rotation=30); ax.xaxis.set_major_formatter(mdates.DateFormatter("%d %b")); ax.set_xlim(right=s.week.iloc[-1] + pd.Timedelta(days=45)); ax.set_title("Unweighted indices, storefront listings", loc="left", color=TXT, fontsize=9) | |
| 97 | 98 | save(fig, "fig_indices") |
| 98 | 99 | |
| 99 | 100 | def fig_qgpai(): |
| 100 | 101 | q = pd.read_csv(P / "qgpai_components.csv", parse_dates=["week"]); fig, axes = plt.subplots(2, 2, figsize=(7.2, 4.8)); axes = axes.ravel() |
| 101 | − ax = axes[0]; ax.fill_between(q.week, q.cpi_lo, q.cpi_hi, color=C[0], alpha=0.18, lw=0); ax.plot(q.week, q.comparable_price_index, color=C[0], lw=2, marker="o", ms=3); ax.set_title("1. Comparable price index (matched groups)", loc="left", color=TXT); ax.set_ylabel("13 Aug = 100") | |
| 102 | + ax = axes[0]; ax.fill_between(q.week, q.cpi_lo, q.cpi_hi, color=C[0], alpha=0.18, lw=0); ax.plot(q.week, q.comparable_price_index, color=C[0], lw=2, marker="o", ms=3); ax.set_title("1. Comparable price index", loc="left", color=TXT); ax.set_ylabel("13 Aug = 100") | |
| 102 | 103 | ax = axes[1]; ax.plot(q.week, q.promo_share, color=C[1], lw=2, marker="o", ms=3); ax.plot(q.week, q.circular_depth_median, color=C[3], lw=2, marker="s", ms=3) |
| 103 | 104 | ax.text(q.week.iloc[-1], q.promo_share.iloc[-1], " share promoted\n (storefront, inferred)", fontsize=7, va="center", color=TXT); ax.text(q.week.iloc[-1], q.circular_depth_median.iloc[-1], " circular depth\n (observed, median log)", fontsize=7, va="center", color=TXT) |
| 104 | 105 | ax.set_title("2. Promotion intensity", loc="left", color=TXT); ax.set_ylim(0) |
| 105 | − ax = axes[2]; ax.fill_between(q.week, q.disp_lo, q.disp_hi, color=C[2], alpha=0.18, lw=0); ax.plot(q.week, q.dispersion_median_gap, color=C[2], lw=2, marker="o", ms=3); ax.set_title("3. Retail price dispersion (median log gap)", loc="left", color=TXT); ax.set_ylim(0) | |
| 106 | + ax = axes[2]; ax.fill_between(q.week, q.disp_lo, q.disp_hi, color=C[2], alpha=0.18, lw=0); ax.plot(q.week, q.dispersion_median_gap, color=C[2], lw=2, marker="o", ms=3); ax.set_title("3. Dispersion (median log gap)", loc="left", color=TXT); ax.set_ylim(0) | |
| 106 | 107 | ax = axes[3] |
| 107 | − for col, name, c in [("basket_K1", "best single banner", C[0]), ("basket_K2", "two banners", C[1]), ("basket_unconstrained", "unconstrained", C[2])]: | |
| 108 | − ax.plot(q.week, q[col], color=c, lw=2, marker="o", ms=3); ax.text(q.week.iloc[-1], q[col].iloc[-1], " " + name, fontsize=7, va="center", color=TXT) | |
| 109 | − ax.set_title("4. Feasible basket cost (CAD, fixed block)", loc="left", color=TXT); ax.set_ylim(0) | |
| 110 | − for ax in axes: ax.tick_params(axis="x", rotation=30) | |
| 108 | + for col, name, c, dy in [("basket_K1", "best single banner", C[0], 1.5), ("basket_K2", "two banners", C[1], 0.0), ("basket_unconstrained", "unconstrained", C[2], -1.5)]: | |
| 109 | + ax.plot(q.week, q[col], color=c, lw=2, marker="o", ms=3); ax.text(q.week.iloc[-1], q[col].iloc[-1] + dy, " " + name, fontsize=7, va="center", color=c) | |
| 110 | + ax.set_title("4. Basket cost (CAD, fixed block)", loc="left", color=TXT); ax.set_ylim(0) | |
| 111 | + for ax in axes: ax.tick_params(axis="x", rotation=30); ax.xaxis.set_major_formatter(mdates.DateFormatter("%d %b")) | |
| 111 | 112 | fig.suptitle("QGPAI components (component 5, access & affordability, not computable)", x=0.01, ha="left", color=TXT2, fontsize=9) |
| 112 | 113 | save(fig, "fig_qgpai") |
| 113 | 114 | |
@@ -139,7 +140,7 @@ def fig_store_mixture(): | ||
| 139 | 140 | sm = pd.read_csv(P / "modelA_within_banner_mixture.csv").sort_values("share_multi", ascending=True).tail(12) |
| 140 | 141 | fig, axes = plt.subplots(1, 2, figsize=(7.4, 2.9), gridspec_kw={"width_ratios": [1.4, 1]}) |
| 141 | 142 | ax = axes[0]; ax.step(e.ts, e.price, where="post", color=C[0], lw=1.2); ax.scatter(e.ts, e.price, color=C[0], s=8, zorder=3) |
| 142 | − ax.set_ylabel("Posted price (CAD)"); ax.set_title("A. One listing (Metro, mayonnaise 890 mL): every logged price change", loc="left", color=TXT, fontsize=8.5); ax.tick_params(axis="x", rotation=30) | |
| 143 | + ax.set_ylabel("Posted price (CAD)"); ax.set_title("A. One Metro listing: every logged price change", loc="left", color=TXT, fontsize=8.5); ax.tick_params(axis="x", rotation=30); ax.xaxis.set_major_formatter(mdates.DateFormatter("%d %b")) | |
| 143 | 144 | ax = axes[1]; ax.barh(sm.banner, sm.share_multi, color=C[0], height=0.6); ax.set_xlabel("Share of listing-weeks with > 1 price value"); ax.set_title("B. Multi-price weeks by banner", loc="left", color=TXT); ax.grid(axis="y", visible=False) |
| 144 | 145 | for i, (v, r_) in enumerate(zip(sm.share_multi, sm.median_log_range_multi)): ax.text(v, i, f" {v:.2f} (range {r_:.2f})", va="center", fontsize=7, color=TXT2) |
| 145 | 146 | save(fig, "fig_store_mixture") |
modified
src/reports/make_tables.py
+30 −14
@@ -6,25 +6,41 @@ log = get_logger("tables"); P = path("processed"); T = ROOT / "paper" / "tables" | ||
| 6 | 6 | def tex_escape(s): |
| 7 | 7 | return str(s).replace("&", "\\&").replace("%", "\\%").replace("_", "\\_").replace("#", "\\#") |
| 8 | 8 | |
| 9 | −def table(df, fname, caption, label, note, colfmt=None, float_fmt="{:.3f}", index=False): | |
| 9 | +def table(df, fname, caption, label, note, colfmt=None, float_fmt="{:.3f}", index=False, wide=False): | |
| 10 | 10 | df = df.copy() |
| 11 | 11 | for c in df.columns: |
| 12 | − if df[c].dtype.kind == "f": df[c] = df[c].map(lambda x: "" if pd.isna(x) else float_fmt.format(x)) | |
| 12 | + if df[c].dtype.kind == "f" and df[c].dropna().apply(lambda x: float(x).is_integer()).all() and df[c].notna().any() and df[c].abs().max() >= 10: | |
| 13 | + df[c] = df[c].map(lambda x: "" if pd.isna(x) else f"{int(x):,}".replace(",", "\\,")) | |
| 14 | + elif df[c].dtype.kind == "f": df[c] = df[c].map(lambda x: "" if pd.isna(x) else float_fmt.format(x)) | |
| 13 | 15 | elif df[c].dtype.kind in "iu": df[c] = df[c].map(lambda x: f"{x:,}".replace(",", "\\,")) |
| 14 | 16 | else: df[c] = df[c].map(tex_escape) |
| 15 | 17 | cols = [tex_escape(c) for c in df.columns]; colfmt = colfmt or ("l" + "r" * (len(cols) - 1)) |
| 16 | 18 | body = " \\\\\n".join(" & ".join(map(str, r)) for r in df.values) |
| 17 | − tex = f"""\\begin{{table}}[htbp]\\centering | |
| 18 | −\\begin{{threeparttable}} | |
| 19 | −\\caption{{{caption}}}\\label{{{label}}} | |
| 20 | −\\small | |
| 21 | −\\begin{{tabular}}{{{colfmt}}} | |
| 19 | + tab = f"""\\begin{{tabular}}{{{colfmt}}} | |
| 22 | 20 | \\toprule |
| 23 | 21 | {' & '.join(cols)} \\\\ |
| 24 | 22 | \\midrule |
| 25 | 23 | {body} \\\\ |
| 26 | 24 | \\bottomrule |
| 27 | −\\end{{tabular}} | |
| 25 | +\\end{{tabular}}""" | |
| 26 | + if wide: # threeparttable conflicts with resized boxes ("Missing \endgroup"): caption + adjustbox + notes in a minipage | |
| 27 | + tex = f"""\\begin{{table}}[htbp]\\centering | |
| 28 | +\\caption{{{caption}}}\\label{{{label}}} | |
| 29 | +\\small | |
| 30 | +\\begin{{adjustbox}}{{max width=\\textwidth}} | |
| 31 | +{tab} | |
| 32 | +\\end{{adjustbox}} | |
| 33 | +\\begin{{minipage}}{{\\textwidth}}\\footnotesize\\vspace{{2pt}} | |
| 34 | +{note} | |
| 35 | +\\end{{minipage}} | |
| 36 | +\\end{{table}} | |
| 37 | +""" | |
| 38 | + else: | |
| 39 | + tex = f"""\\begin{{table}}[htbp]\\centering | |
| 40 | +\\begin{{threeparttable}} | |
| 41 | +\\caption{{{caption}}}\\label{{{label}}} | |
| 42 | +\\small | |
| 43 | +{tab} | |
| 28 | 44 | \\begin{{tablenotes}}[flushleft]\\footnotesize |
| 29 | 45 | \\item {note} |
| 30 | 46 | \\end{{tablenotes}} |
@@ -41,7 +57,7 @@ def main(): | ||
| 41 | 57 | t1 = f[["company", "banner", "channel", "listings", "active", "median_days_obs", "price_events", "unit_price_cov", "brand_cov"]].head(24).copy() |
| 42 | 58 | t1.columns = ["Company", "Banner", "Channel", "Listings", "Active", "Median days", "Price events", "Unit price", "Brand"] |
| 43 | 59 | table(t1, "tab_coverage_feeds.tex", "Coverage of the 24 largest source feeds (snapshot of 1 November 2026)", "tab:coverage", |
| 44 | − "Listings = all uids ever observed in the feed (active and deactivated); Active = listed on 1 Nov 2026; Median days = median observation span; Price events = rows in the price log; Unit price / Brand = share of active listings with a parsable package size (re-parsed) / a brand field. Channel: online storefront vs weekly circular (Flipp, postal code H2X 1Y4). Full table of 54 feeds in the replication package.", float_fmt="{:.2f}") | |
| 60 | + "Listings = all uids ever observed in the feed (active and deactivated); Active = listed on 1 Nov 2026; Median days = median observation span; Price events = rows in the price log; Unit price / Brand = share of active listings with a parsable package size (re-parsed) / a brand field. Channel: online storefront vs weekly circular (Flipp, postal code H2X 1Y4). Full table of 54 feeds in the replication package.", float_fmt="{:.2f}", wide=True) | |
| 45 | 61 | # T2 attrition |
| 46 | 62 | att = pd.read_csv(P / "attrition_listings.csv"); att.columns = ["Step", "Listings", "Share of initial"] |
| 47 | 63 | table(att, "tab_attrition.tex", "Listing-level sample attrition", "tab:attrition", "Each row applies the stated filter to the previous row. Matching tiers A--C shown; the primary analysis uses A+B.", colfmt="p{9cm}rr") |
@@ -50,7 +66,7 @@ def main(): | ||
| 50 | 66 | t3 = v[["tier", "n", "same", "different", "unsure", "precision_strict", "ci_lo", "ci_hi", "upc_precision", "upc_recall"]].copy() |
| 51 | 67 | t3.columns = ["Tier", "n", "Same", "Different", "Unsure", "Precision", "CI low", "CI high", "UPC precision", "UPC recall"] |
| 52 | 68 | table(t3, "tab_match_validation.tex", "Matching validation: stratified sample (60 pairs per tier) and UPC ground truth", "tab:validation", |
| 53 | − "Precision = share labelled SAME (Wilson 95\\% CI); labels by Claude (claude-opus-5-5) as second annotator with full operator review of every pair; PI adjudication pending. UPC columns: precision/recall of the tier's text rule on all Metro\\,$\\times$\\,Super~C candidate pairs within brand\\,$\\times$\\,package blocks, where the shared GS1 code is the truth (B: identical token sets; C: token-sort ratio $\\ge 0.90$). Tier D has no UPC benchmark.") | |
| 69 | + "Precision = share labelled SAME (Wilson 95\\% CI); labels by Claude (claude-opus-5-5) as second annotator with full operator review of every pair; PI adjudication pending. UPC columns: precision/recall of the tier's text rule on all Metro\\,$\\times$\\,Super~C candidate pairs within brand\\,$\\times$\\,package blocks, where the shared GS1 code is the truth (B: identical token sets; C: token-sort ratio $\\ge 0.90$). Tier D has no UPC benchmark.", wide=True) | |
| 54 | 70 | # T4 dispersion summary |
| 55 | 71 | rows = [] |
| 56 | 72 | for k, lab in [("gap_all", "All group-weeks"), ("gap_same_company", "Same company (two banners)"), ("gap_cross_company", "Different companies")]: |
@@ -62,7 +78,7 @@ def main(): | ||
| 62 | 78 | fe = pd.read_csv(P / "modelA_banner_effects.csv").sort_values("coef") |
| 63 | 79 | t5 = fe[["banner", "coef", "se", "se_twoway", "n_listing_weeks"]].copy(); t5.columns = ["Banner", "Coefficient", "s.e. (group)", "s.e. (group+banner)", "Listing-weeks"] |
| 64 | 80 | table(t5, "tab_banner_effects.tex", f"Banner fixed effects on log posted price within product-week (reference: {A['banner_fe_reference']})", "tab:bannerfe", |
| 65 | − "Model A: $\\log p_{irw} = \\alpha_{iw} + \\gamma_r + \\varepsilon_{irw}$ on matched groups (tiers A+B), storefront channel, non-ambiguous group-weeks. Standard errors clustered by match group, and two-way by group and banner. Coefficients are relative posted-price levels for the same product in the same week; banners with few listing-weeks are imprecisely estimated.", float_fmt="{:.3f}") | |
| 81 | + "Model A: $\\log p_{irw} = \\alpha_{iw} + \\gamma_r + \\varepsilon_{irw}$ on matched groups (tiers A+B), storefront channel, non-ambiguous group-weeks. Standard errors clustered by match group, and two-way by group and banner. Coefficients are relative posted-price levels for the same product in the same week; banners with few listing-weeks are imprecisely estimated.", float_fmt="{:.3f}", wide=False) | |
| 66 | 82 | # T6 promotions |
| 67 | 83 | rows = [("Listing-weeks promoted (inferred, $p \\le 0.9\\times$ trailing 8-week mode)", B["promo_share_listing_weeks"]), ("Median depth (log ref/p), promoted weeks", B["promo_depth_median"]), |
| 68 | 84 | ("Agreement with current on-sale flag (last week)", B["promo_vs_on_sale_now_agreement"]), ("S.d. weekly $\\Delta\\log p$, raw", B["sd_raw_all"]), ("S.d. weekly $\\Delta\\log$ reference", B["sd_ref_all"]), |
@@ -86,13 +102,13 @@ def main(): | ||
| 86 | 102 | r[nm] = f"{c['coefs'][t]:.3f} ({c['ses'][t]:.3f})" |
| 87 | 103 | rows.append(r) |
| 88 | 104 | table(pd.DataFrame(rows), "tab_spillovers.tex", "Descriptive lead--lag associations between a banner's weekly log price change and rivals' changes (Model C)", "tab:spillovers", |
| 89 | − "Dependent variable: $\\Delta\\log p$ of banner $r$ for product $i$ in week $w$; regressors: mean $\\Delta\\log p$ of the other banners in the match group at $w-k$ (lags) and $w+k$ (leads, placebo). Banner and week fixed effects; s.e. clustered by match group. Contemporaneous terms absorb common shocks and are not causal; a non-zero lead coefficient indicates timing noise or anticipation and is reported as a failed placebo.", colfmt="lr" + "r" * 6) | |
| 105 | + "Dependent variable: $\\Delta\\log p$ of banner $r$ for product $i$ in week $w$; regressors: mean $\\Delta\\log p$ of the other banners in the match group at $w-k$ (lags) and $w+k$ (leads, placebo). Banner and week fixed effects; s.e. clustered by match group. Contemporaneous terms absorb common shocks and are not causal; a non-zero lead coefficient indicates timing noise or anticipation and is reported as a failed placebo.", colfmt="lr" + "r" * 6, wide=True) | |
| 90 | 106 | # T8 indices |
| 91 | 107 | rows = [] |
| 92 | 108 | for k, lab in [("storefront_posted", "Storefront, posted price"), ("storefront_unit", "Storefront, unit price"), ("all_channels_posted", "All channels, posted price")]: |
| 93 | 109 | s = D[k]; rows.append({"Sample": lab, "Naive mean": s["naive_mean_change"], "Naive median": s["naive_median_change"], "Chained Jevons": s["jevons_chained_change"], "Fixed Jevons": s["fixed_jevons_change"], "Fixed Dutot": s["fixed_dutot_change"], "Fixed Carli": s["fixed_carli_change"], "Balanced n": s["n_balanced"]}) |
| 94 | 110 | table(pd.DataFrame(rows), "tab_indices.tex", "Cumulative price change over 11 weeks (13 Aug $\\to$ 22 Oct 2026) under alternative unweighted index formulas", "tab:indices", |
| 95 | − f"No expenditure weights exist: none of these is a consumer price index. Bootstrap (999 listing resamples) 95\\% CI for the storefront chained Jevons: [{D['bootstrap']['jevons_ci95'][0]:.3f}, {D['bootstrap']['jevons_ci95'][1]:.3f}]; naive-mean minus Jevons gap: {D['bootstrap']['gap_mean']:.3f} [{D['bootstrap']['gap_ci95'][0]:.3f}, {D['bootstrap']['gap_ci95'][1]:.3f}]. Statistics Canada CPI, Quebec, food purchased from stores: Aug$\\to$Sep 2026 {D['statcan']['cpi_change_aug_sep']:+.3f} (our chained Jevons, monthly means, {D['statcan']['ours_jevons_aug_sep']:+.3f}); 12-month CPI change to Sep 2026 {D['statcan']['cpi_yoy_sep']:+.3f}.") | |
| 111 | + f"No expenditure weights exist: none of these is a consumer price index. Bootstrap (999 listing resamples) 95\\% CI for the storefront chained Jevons: [{D['bootstrap']['jevons_ci95'][0]:.3f}, {D['bootstrap']['jevons_ci95'][1]:.3f}]; naive-mean minus Jevons gap: {D['bootstrap']['gap_mean']:.3f} [{D['bootstrap']['gap_ci95'][0]:.3f}, {D['bootstrap']['gap_ci95'][1]:.3f}]. Statistics Canada CPI, Quebec, food purchased from stores: Aug$\\to$Sep 2026 {D['statcan']['cpi_change_aug_sep']:+.3f} (our chained Jevons, monthly means, {D['statcan']['ours_jevons_aug_sep']:+.3f}); 12-month CPI change to Sep 2026 {D['statcan']['cpi_yoy_sep']:+.3f}.", wide=True) | |
| 96 | 112 | # T9 basket |
| 97 | 113 | bs = pd.read_csv(P / "modelE_block_sensitivity.csv"); bs.columns = ["Banners (B)", "Items", "Cost K=1", "Cost K=2", "Cost unconstrained", "Savings share", "Share captured at K=2"] |
| 98 | 114 | table(bs, "tab_basket.tex", "Fixed-basket cost under banner-count constraints (Model E, weekly means over 11 weeks, CAD)", "tab:basket", |
@@ -114,7 +130,7 @@ def main(): | ||
| 114 | 130 | # T12 QGPAI |
| 115 | 131 | q = pd.read_csv(P / "qgpai_components.csv"); tq = q[["week", "comparable_price_index", "cpi_lo", "cpi_hi", "promo_share", "dispersion_median_gap", "basket_K1", "basket_K2", "basket_unconstrained", "circular_depth_median"]].copy() |
| 116 | 132 | tq["week"] = pd.to_datetime(tq.week).dt.strftime("%d %b"); tq.columns = ["Week", "Comparable index", "CI low", "CI high", "Promo share", "Dispersion", "Basket K=1", "Basket K=2", "Basket uncon.", "Circular depth"] |
| 117 | − table(tq, "tab_qgpai.tex", "QGPAI components by circular week (Thursday start), 2026", "tab:qgpai", "Component 5 (access and affordability) is not computed: no store geography or income data are joined. Comparable index: chained Jevons over matched groups (A+B), base 13 Aug = 100, bootstrap band over groups; Dispersion: median within-group max$-$min log gap; Basket: Model E (B = 5). Composites are illustrative only and reported in the replication package.", float_fmt="{:.3f}") | |
| 133 | + table(tq, "tab_qgpai.tex", "QGPAI components by circular week (Thursday start), 2026", "tab:qgpai", "Component 5 (access and affordability) is not computed: no store geography or income data are joined. Comparable index: chained Jevons over matched groups (A+B), base 13 Aug = 100, bootstrap band over groups; Dispersion: median within-group max$-$min log gap; Basket: Model E (B = 5). Composites are illustrative only and reported in the replication package.", float_fmt="{:.3f}", wide=True) | |
| 118 | 134 | log.info("tables done") |
| 119 | 135 | |
| 120 | 136 | if __name__ == "__main__": |
added
tests/test_outputs.py
+39 −0
@@ -0,0 +1,39 @@ | ||
| 1 | +"""Smoke and consistency tests on pipeline outputs (run after `make phase4 phase5 robust`).""" | |
| 2 | +import json, pandas as pd, numpy as np, pytest | |
| 3 | +from src.common import ROOT | |
| 4 | +P = ROOT / "data" / "processed"; E = ROOT / "experiments" | |
| 5 | + | |
| 6 | +def test_counts_match_source(): | |
| 7 | + prod = pd.read_parquet(P / "products.parquet"); assert len(prod) == 124579 and int((prod.active == 1).sum()) == 50760 | |
| 8 | + pl = pd.read_parquet(ROOT / "data" / "interim" / "raw_price_log.parquet"); assert len(pl) == 300613 | |
| 9 | + | |
| 10 | +def test_matched_panel_consistency(): | |
| 11 | + pm = pd.read_parquet(P / "panel_matched_AB.parquet") | |
| 12 | + assert (pm.groupby(["group_id", "week"]).banner.nunique() >= 2).all() | |
| 13 | + assert pm.week.nunique() == 11 and pm.p_mode.gt(0).all() | |
| 14 | + | |
| 15 | +def test_model_a_numbers_reproducible_from_stats(): | |
| 16 | + A = json.load(open(E / "modelA_summary.json")); st = pd.read_csv(P / "modelA_groupweek_stats.csv") | |
| 17 | + assert abs(st.gap.median() - A["gap_all"]["median"]) < 1e-9 and len(st) == A["gap_all"]["n"] | |
| 18 | + assert A["placebo_ar1_mean"] < 0.05 < A["ar1_rho"] | |
| 19 | + | |
| 20 | +def test_model_b_internal_consistency(): | |
| 21 | + B = json.load(open(E / "modelB_summary.json")); ep = pd.read_parquet(P / "modelB_episodes.parquet") | |
| 22 | + assert len(ep) == B["n_episodes"] and int((~ep.censored).sum()) == B["n_uncensored"] | |
| 23 | + assert 0 < B["promo_share_listing_weeks"] < 0.5 and B["ratio_ci95"][0] <= B["ratio_all"] <= B["ratio_ci95"][1] | |
| 24 | + | |
| 25 | +def test_indices_base_100(): | |
| 26 | + d = pd.read_csv(P / "modelD_indices.csv") | |
| 27 | + for s, g in d.groupby("sample"): | |
| 28 | + assert abs(g["index"].iloc[0] - 100) < 1e-9 and abs(g.jevons.iloc[0] - 100) < 1e-9 | |
| 29 | + | |
| 30 | +def test_basket_monotone_in_K(): | |
| 31 | + r = pd.read_csv(P / "modelE_results.csv"); r5 = r[(r.B == 5) & r.single.isna() & r.fixed_cost.isna()] | |
| 32 | + for w, g in r5.groupby("week"): | |
| 33 | + c = g.sort_values("K").cost.values; assert all(c[i] >= c[i + 1] - 1e-6 for i in range(len(c) - 1)) | |
| 34 | + | |
| 35 | +def test_forecast_no_leakage_columns(): | |
| 36 | + r = pd.read_csv(P / "modelF_rolling_results.csv"); assert r.origin_t.min() >= 6 and (r.n > 0).all() | |
| 37 | + | |
| 38 | +def test_qgpai_components_present(): | |
| 39 | + q = pd.read_csv(P / "qgpai_components.csv"); assert len(q) == 11 and q.access_affordability.isna().all() | |
| 40 | ||