SPB Git

spb/wp37_uqo

Public

phase0: feasibility report, settings, run ledger, API credential log (no secrets), DB profiling logs

Simon-Pierre Boucher committed (Nov 1, 2026) parent 23a2126 Browse files

4 changed files +251 −0

added config/settings.yaml +31 −0
@@ -0,0 +1,31 @@
1 +# WP37 — global settings (single source of truth for paths, seeds, snapshot identity)
2 +project: wp37-uqo
3 +snapshot:
4 + id: foodka-snapshot-20261101
5 + path: data/raw/foodka-snapshot-20261101/foodka-snapshot.db
6 + sha256: 3cd7684d6596b6759eb78927d0341aa63cd288529a7e61576e180ae0a2059a94
7 + taken_utc: "2026-11-01T07:57:11Z"
8 + foodka_commit: 2c765231c826b4505a4e56fa0275e8c19fe689c6
9 + sources_registry: data/raw/foodka-snapshot-20261101/sources.json
10 +seed: 20261101
11 +timezone_display: America/Toronto
12 +paths:
13 + interim: data/interim
14 + processed: data/processed
15 + figures: paper/figures
16 + tables: paper/tables
17 + logs: logs
18 + reports: reports
19 +# Panel window actually observed in the snapshot (event log, UTC)
20 +panel:
21 + start: "2026-08-12"
22 + end: "2026-11-01"
23 + observation_cadence_hours_median: 7.2
24 +# Scope decisions fixed in Phase 1 (see research/preregistration.md once frozen)
25 +scope:
26 + exclude_sources: [saq] # provincial alcohol monopoly, uniform posted prices, not a grocery retailer
27 + non_grocery_categories: ["Santé et beauté", "Maison et entretien", "Animaux", "Bébé", "Alcool"]
28 + pharmacy_sources: [pharmaprix, jean_coutu, brunet, uniprix]
29 +statcan:
30 + cpi_table: 18100004 # CPI monthly NSA; geo Quebec=11, Montréal=13; product Food purchased from stores=4
31 + retail_prices_table: 18100245 # Monthly average retail prices for selected products; geo Quebec
added pyproject.toml +29 −0
@@ -0,0 +1,29 @@
1 +[project]
2 +name = "wp37-uqo"
3 +version = "0.1.0"
4 +description = "WP37 — The Hidden Architecture of Grocery Prices (Quebec): reproducible research pipeline"
5 +requires-python = ">=3.12,<3.13"
6 +dependencies = [
7 + "pandas>=2.2,<3",
8 + "numpy>=1.26,<3",
9 + "scipy>=1.13",
10 + "statsmodels>=0.14",
11 + "linearmodels>=6.0",
12 + "pyfixest>=0.25",
13 + "scikit-learn>=1.5",
14 + "lightgbm>=4.3",
15 + "matplotlib>=3.9",
16 + "duckdb>=1.0",
17 + "pulp>=2.8",
18 + "pyyaml>=6.0",
19 + "pytest>=8.0",
20 + "requests>=2.32",
21 + "rapidfuzz>=3.9",
22 + "openai>=1.50",
23 + "anthropic>=0.40",
24 + "tqdm>=4.66",
25 + "pyarrow>=16",
26 + "python-dotenv>=1.0",
27 +]
28 +[tool.pytest.ini_options]
29 +testpaths = ["tests"]
added reports/phase0_feasibility.md +94 −0
@@ -0,0 +1,94 @@
1 +# Phase 0 — Feasibility report (WP37)
2 +
3 +Date: 2026-11-01 (UTC). Author of record: S.-P. Boucher (UQO), pipeline executed by Claude Code.
4 +
5 +## 1. Working directory and repository
6 +
7 +- Project created at `Recherche-UQO/economie/hidden-architecture-grocery-prices/` from the UQO working-paper template (`_modele-working-paper`). Nothing pre-existing was overwritten; WP numbers 1–35 are taken, WP36 is an unnumbered stub (`economie/used-car-market-liquidity`), so **WP37** is free.
8 +- Git initialised locally (`main`). Remote `gitsrv:srv/git/wp37_uqo.git` not yet created (creation on spbgit requires the PI's go-ahead; nothing is published externally).
9 +- Mandate stored verbatim in `CLAUDE.md`.
10 +
11 +## 2. Compute, toolchain
12 +
13 +| Item | Observed |
14 +|---|---|
15 +| Machine | Laptop, 18 cores, 48 GiB RAM, 1.0 TiB free (sufficient: raw DB is 165 MB). MacLustr cluster (29 nodes) available but not required. |
16 +| Python | 3.12 venv (`uv`), deps pinned in `requirements-lock.txt` (pandas, statsmodels, linearmodels, pyfixest, scikit-learn, lightgbm, duckdb, pulp, matplotlib, pytest…) |
17 +| R | 4.x available (`/opt/homebrew/bin/R`) — fallback only |
18 +| LaTeX | TeX Live, latexmk 4.88 ; pandoc present |
19 +| Seeds | `config/settings.yaml: seed: 20261101` used by every stochastic step (bootstrap, CV folds, sampling) |
20 +
21 +## 3. Data access — what actually exists
22 +
23 +**Food-Ka is owned by the PI**; the production SQLite database (`M2U64:~/apps/food-ka/data/foodka.db`, WAL mode) was copied with a consistent `.backup` from a read-only connection (production untouched). Checksum verified. Provenance in `data/raw/README.md`.
24 +
25 +Public cross-check (2026-11-01 ~08:00 UTC): homepage shows 50 760 products, 54 banners, 8 631 on promotion — identical to the snapshot's `active=1` rows. The mandate's figures (50 886 / 54 / 8 590) were the homepage values a few hours earlier; they are **source-feed listings, not unique products**. `robots.txt` disallows `/api/` to crawlers; the API was queried once for this cross-check only and is not a data source.
26 +
27 +### 3.1 Schema (observed)
28 +
29 +`products` (124 579 rows: 50 760 active, 73 819 deactivated) — uid = `source:external_id`, name, brand, category (18 canonical), category_raw, size_label, price, regular_price, price_label, on_sale, unit_price, unit_price_label, in_stock, details (JSON), first_seen, last_seen, updated_at, miss_count, active.
30 +`price_log` (300 613 rows: uid, ts, price) — **event log**: one row at first observation and one row at every *change* of `price` (NULL = price disappeared from display). No row when the price is unchanged. `regular_price`, `on_sale`, `in_stock` are **not** historised.
31 +`sync_log` (15 630 rows) — one row per connector pass (57 sources × ~255–308 passes, median gap **7.2 h**, 2026-08-12 → 2026-11-01), with found/added/updated/removed counts.
32 +`product_links` (2 537 uids in 1 103 cross-banner groups) — Food-Ka's own brand+size+name matching (not GTIN based).
33 +`off_cache` (25 630) — Open Food Facts barcodes attached by a conservative brand/name/size lookup (derived, not source-provided).
34 +
35 +### 3.2 Data-generating process (from the connector code, read-only copy in `data/raw/foodka-src/`)
36 +
37 +- 57 active source feeds = **54 with active products**, belonging to far fewer **retail companies**: Loblaw (maxi, provigo, club_entrepot + 3 flyers + inter_marche, axep? no: Axep/Richelieu/Marché Ami are Metro affiliates), Metro Inc. (metro, superc + 2 flyers + richelieu, marche_ami, axep), Sobeys (iga + flyer, tradition, bonichoix, rachelle_bery), Walmart, Costco, Adonis (Metro Inc.), Avril, independents, 4 pharmacies, SAQ. A `retailers` table (company → banner → feed → channel) will be built in Phase 1.
38 +- **Two channels**: (a) *online storefront* feeds (Shopify/WooCommerce JSON, Loblaw/Metro/Walmart/Costco/IGA sites scraped through residential proxy pools without store selection); (b) *weekly circulars* via Flipp for postal code **H2X 1Y4 (Montréal)** with `valid_from`/`valid_to`, `original_price`, `dollars_off`, fine print.
39 +- **Geography**: no store identifier anywhere. Circulars are regional (Montréal zone); online prices are whatever store the retailer's site assigned to the proxy session. → **Store-level geography is unavailable** (H4 and travel-cost components of Model E cannot be estimated from Food-Ka).
40 +- **Identifiers**: Metro and Super C `external_id` are valid GS1 UPC-A codes (100 % check-digit valid, n = 1 954 active). Shopify/Walmart/Loblaw ids are internal SKUs (check-digit validity ≈ 10 %, i.e. chance). Exact GTIN matching is therefore only possible within Metro Inc.; everything else requires validated probabilistic matching.
41 +
42 +### 3.3 Critical measurement finding — store-mixture noise in scraped banners
43 +
44 +For anti-bot banners (club_entrepot, metro, maxi, superc, provigo, walmart) the event log shows **high-frequency oscillation among a small set of price values** (e.g. Metro mayonnaise 890 mL alternating 5.77 / 6.99 / 7.49 / 7.79 / 7.99 within the same day; 54–68 % of change events revert to the previous-but-one value; median 4 distinct values per product; week-to-week Jaccard of value sets ≈ 0.5). This is consistent with **each scrape being served by a different (unknown) store**, not with genuine intra-day repricing. Consequences, to be handled in Phase 1 and preregistered in Phase 3:
45 +- a banner-level "posted price" at time *t* must be defined as a statistic of the daily/weekly value set (mode, max = likely shelf regular, min = likely best store/promo), with sensitivity analysis;
46 +- intra-week volatility for these banners is **not** interpretable as price flexibility; H2 must be tested on weekly aggregates and separately on circular feeds (clean weekly cycles);
47 +- the amplitude of the value set is itself a (store-unidentified) lower bound on **within-banner cross-store dispersion** — a usable, honestly-labelled quantity.
48 +Shopify/WooCommerce feeds (single online store) show far fewer events and no such cycling.
49 +
50 +### 3.4 Variable availability (summary; full inventory in Phase 1)
51 +
52 +| Variable | Status |
53 +|---|---|
54 +| product name, brand, category, size_label | confirmed (brand missing for several feeds: walmart 87 %, adonis/tt/akhavan/bocoboco 100 %) |
55 +| UPC/GTIN | confirmed for Metro/Super C only; derivable-unreliable via OFF barcode elsewhere |
56 +| unit price ($/100 g, $/100 ml, $/un) | derivable (Food-Ka's parser); missing where size_label absent (giant_tiger 92 %, nuvo 100 %, most Flipp feeds) |
57 +| current price history | confirmed (event log, 81 days) |
58 +| regular/reference price history | **unavailable** (current state only); circulars carry `original_price` per item-week |
59 +| promotion flag history | unavailable → must be **inferred** from price paths; validated against current `on_sale` and circular items |
60 +| observation timestamps | confirmed (`sync_log` per source; product-level presence between events inferred) |
61 +| circular validity | confirmed (Flipp feeds) |
62 +| availability/in_stock | current only; unreliable (NULL for 23 %) |
63 +| store / geography | **unavailable** |
64 +| quantities / expenditure weights | unavailable (no transactions) |
65 +
66 +## 4. External benchmarks
67 +
68 +Statistics Canada WDS reachable: table 18-10-0004-01 (CPI monthly NSA, Quebec = member 11, Montréal = 13; "Food purchased from stores" = member 4; latest 2026-09) and 18-10-0245-01 (monthly average retail prices for 110 selected products, Quebec; latest 2026-08). Overlap with the Food-Ka window is only **Aug–Sep 2026** (2 months) — validation of QGPAI against official indices will necessarily be weak and labelled as such.
69 +
70 +## 5. API credentials (minimal tests, 2026-11-01T08:02Z)
71 +
72 +| Provider | Model | Result |
73 +|---|---|---|
74 +| Anthropic | claude-opus-5-5, claude-sonnet-5-5, claude-haiku-5-5 | OK (end_turn, 18 in / 4 out tokens each) |
75 +| OpenAI | gpt-5.4, gpt-5.4-mini, gpt-5.4-nano | OK |
76 +
77 +Planned LLM use is limited and auxiliary: (i) second annotator for the stratified match-validation sample (human = PI adjudication remains the reference), (ii) nothing in estimation. Every call is logged with model id, tokens and cost.
78 +
79 +## 6. GO / NO-GO gates
80 +
81 +| Gate | Decision | Basis |
82 +|---|---|---|
83 +| G1 Authorised data access | **GO** | owner export, read-only copy, checksum |
84 +| G2 Longitudinal panel | **GO (partial)** | 81-day event log, ~3.3 passes/day/source; reconstructable daily panel; regular-price history absent |
85 +| G3 Cross-banner matching | **GO (partial)** | exact UPC only Metro↔Super C; probabilistic matching + validation elsewhere; expect a few thousand matched products, not 50 k |
86 +| G4 Promotion dynamics | **GO (partial)** | circulars: clean weekly cycles with reference prices; online feeds: promotion state inferred from price paths and store-mixture noise |
87 +| G5 Competitive spillovers | **GO descriptive only** | weekly banner-level responses; no exogenous variation identified a priori → no causal claims |
88 +| G6 Store geography / travel costs | **NO-GO** | no store identifiers; H4 untestable; Model E limited to banner-count constraints; prospective plan required |
89 +| G7 Price indices vs StatCan | **GO (weak)** | 2-month overlap only |
90 +| G8 Compute | **GO** | laptop sufficient |
91 +
92 +## 7. Expected compute
93 +
94 +Whole pipeline (panel reconstruction, matching, fixed-effects models, bootstraps, MIP baskets, rolling-origin forecasts) is expected to run in well under one hour on the laptop; no cluster job required.
added requirements-lock.txt +97 −0
@@ -0,0 +1,97 @@
1 +annotated-types==0.8.0
2 +anthropic==2.3.0
3 +anyio==4.15.1
4 +asttokens==3.0.2
5 +babel==2.18.0
6 +certifi==2026.10.23
7 +cffi==2.1.1
8 +charset-normalizer==3.5.2
9 +click==8.5.0
10 +cloudpickle==3.1.2
11 +contourpy==1.4.0
12 +cpsat-logutils==1.0.0
13 +cycler==0.12.1
14 +docstring-parser==0.18.0
15 +duckdb==2.0.0
16 +executing==2.3.0
17 +faicons==0.2.2
18 +fonttools==4.68.0
19 +formulaic==1.2.2
20 +great-tables==1.1.0
21 +h11==0.16.0
22 +htmltools==0.7.0
23 +httpcore2==2.16.0
24 +httpx2==2.16.0
25 +idna==3.20
26 +importlib-metadata==10.1.0
27 +importlib-resources==7.1.0
28 +iniconfig==2.3.1
29 +interface-meta==2.0.1
30 +ipython==9.18.0
31 +ipython-pygments-lexers==1.1.1
32 +jedi==0.20.1
33 +jiter==0.18.0
34 +joblib==1.6.0
35 +kiwisolver==1.5.1
36 +lightgbm==4.7.0
37 +linearmodels==7.1
38 +lxml==6.1.3
39 +maketables==0.2.0
40 +matplotlib==3.11.2
41 +matplotlib-inline==0.2.2
42 +multimark==0.4.0
43 +mypy-extensions==1.1.0
44 +narwhals==2.28.0
45 +nokap==0.1.0
46 +numpy==2.5.4
47 +openai==3.42.0
48 +opentelemetry-api==1.45.2
49 +orloge==1.0.0
50 +packaging==26.3
51 +pandas==2.3.3
52 +parso==0.8.7
53 +patsy==1.0.3
54 +pexpect==4.9.0
55 +pillow==13.0.0
56 +pluggy==1.7.0
57 +prompt-toolkit==3.0.53
58 +psutil==7.2.2
59 +ptyprocess==0.7.0
60 +pulp==4.2.0
61 +pure-eval==0.2.4
62 +pyarrow==26.0.0
63 +pycparser==3.12
64 +pydantic==2.14.2
65 +pydantic-core==2.50.2
66 +pyfixest==0.60.0
67 +pygments==2.21.0
68 +pyhdfe==0.2.1
69 +pyparsing==3.3.3
70 +pytest==9.2.0
71 +python-dateutil==4.0.0
72 +python-docx==1.2.0
73 +python-dotenv==1.2.4
74 +pytz==2026.6
75 +pyyaml==6.0.3
76 +rapidfuzz==3.14.6
77 +requests==2.34.2
78 +scikit-learn==1.9.1
79 +scipy==1.18.1
80 +seaborn==0.13.2
81 +sniffio==1.3.1
82 +stack-data==0.6.3
83 +statsmodels==0.15.0
84 +tabulate==0.10.0
85 +threadpoolctl==3.7.0
86 +tqdm==4.70.1
87 +traitlets==5.16.1
88 +truststore==0.10.5
89 +typing-extensions==4.16.0
90 +typing-inspection==0.4.4
91 +tzdata==2026.6
92 +urllib3==2.8.0
93 +wcwidth==0.9.4
94 +websockets==17.4
95 +-e file:///Users/simon-pierreboucher/Desktop/UQO/Recherche/Recherche-UQO/economie/hidden-architecture-grocery-prices
96 +wrapt==2.5.2
97 +zipp==4.1.2
98
Menu

Theme