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