SPB Git

spb/wp37_uqo

Public
10.9 KB

# Phase 1 — Data audit and research-grade panel (WP37)

Generated by src/reports/phase1_audit.py from the processed tables on 2026-11-01 09:15 UTC. Snapshot foodka-snapshot-20261101 (sha256 3cd7684d6596b675…).

# 1. Source-to-variable inventory

variable status notes
product name confirmed all feeds; language FR/EN mixed (IGA/Voilà, Adonis, T&T in English)
brand confirmed / partly unavailable missing for 13 % of active listings (Walmart, Adonis, T&T, Akhavan, BocoBoco, Costco feeds lack a brand field; brand often embedded in the name)
UPC / GTIN confirmed (Metro, Super C only) 5,120 active listings with GS1-valid codes; other feeds expose internal SKUs. Open Food Facts barcodes attached by Food-Ka to 913 active listings are derived by name lookup → 'derivable, unreliable', used only as a diagnostic
package size / unit derivable re-parsed from size_label or name: 63.2 % of active listings (Food-Ka's own parser: 43.2 %); missing mainly for Giant Tiger, Nuvo and circular feeds
category confirmed (18 canonical) Food-Ka mapping from each banner's taxonomy; 'Autres' is a residual (19 % of active listings)
retailer / banner / company derivable 54 active feeds → banners → companies (retailers table); ownership basis recorded per feed
store unavailable no store identifier in any feed; online storefronts answered anonymous sessions (store unknown); circulars for postal code H2X 1Y4
current price confirmed event log: first observation + every change (NULL when price disappears)
regular / reference price current state only; circulars per item-week not historised for storefront feeds; original_price in Flipp items
unit price derivable recomputed from price and parsed size; per-kg labels handled; multi-buy 'n / x $' labels flagged
promotion flag current state only historical promotion state must be inferred from price paths (storefront) or is implicit (circular items are promotions by construction)
observation timestamp confirmed (source level) 15,630 connector passes, median gap 7.2 h; product-level presence between events is inferred (see §3)
circular validity confirmed valid_from / valid_to per Flipp item (weekly, Thu→Wed)
availability / stock unreliable in_stock NULL for 23 % of active listings; current state only
geography unavailable see store
quantities / expenditure weights unavailable listings only; no transactions

# 2. Retail hierarchy (feeds → banners → companies)

Active feeds in the registry: 57; with active listings in the snapshot: 54. Channel split: {'circular': 31, 'online_storefront': 25, 'circular_hybrid': 1}.

company banners feeds
Metro Inc. 5 8
Loblaw Companies 5 7
Sobeys / Empire 4 5
Metro Inc. (affiliate) 3 3
Walmart Canada 1 2
Avril Supermarché Santé 1 2
Supermarché PA 2 2
Costco Wholesale Canada 1 2
Loblaw Companies (affiliate) 2 2
Akhavan 1 1
Maturin 1 1
Marché Val-Mont 1 1
Pasquier 1 1
McKesson Canada 1 1
Marché Nuvo 1 1
Société des alcools du Québec 1 1
Supermarché Aurès 1 1
Marché Tau 1 1
Marché Bonanza 1 1
Marché Fu Tai 1 1
Marché C&T 1 1
Aliments Merci 1 1
Les Aliments M&M 1 1
Le Marché Végétarien 1 1
La Boîte à Grains 1 1
Kim Phat 1 1
Groupe Mayrand 1 1
Giant Tiger Stores 1 1
Euro Marché 1 1
Epipresto 1 1
BocoBoco 1 1
Aubut 1 1
Épiceries LOCO 1 1

Pharmacy feeds (4) and the SAQ are kept in the retailers table but excluded from grocery analyses; same-banner storefront/circular pairs are 'channel' links, not cross-banner comparisons.

# 3. Panel reconstruction

price_log is an event log (first observation + each change). A product's price is carried forward between events until last_seen; days are local (America/Toronto). Daily statistics per listing: time-weighted mode, min, max, mean, open/close, number of changes and distinct values. Weekly aggregation uses the Quebec circular week (Thursday→Wednesday). Tests (tests/test_panel.py) verify bounds, that covered hours equal the observation span, and that single-event products have exactly one distinct price.

Limitations: (i) a listing that leaves and re-enters a feed is not logged as a gap unless its price changed — intra-span absence is unobserved; (ii) deactivation requires two consecutive misses (≈14 h), so last_seen dates removals to within one pass.

Panel size: 5,327,181 listing-days over 13 circular weeks (2026-08-06 → 2026-10-29).

# 4. Store-mixture noise (measurement finding)

Share of listing-days with more than one observed price value, and the median relative range (max−min)/mode on those days, for feeds with > 20 000 listing-days:

source product_days multi_price_share median_rel_range_when_multi
club_entrepot 168,906 0.237 0.125
costco 39,304 0.058 0.026
maxi 149,947 0.056 0.143
metro 552,776 0.056 0.188
avril 35,974 0.033 0.200
superc 231,371 0.030 0.214
tt 33,947 0.029 0.175
adonis 106,131 0.025 0.200
iga 108,586 0.025 0.194
provigo 135,183 0.023 0.231
walmart 129,980 0.020 0.143
superc_flyer 35,634 0.014 0.257
mayrand 36,455 0.014 0.164
metro_flyer 45,600 0.013 0.334
iga_flyer 45,980 0.013 0.357
boite_a_grains 463,283 0.012 0.177
pa 275,853 0.012 0.251
adonis_flyer 25,745 0.010 0.247
giant_tiger 761,998 0.008 0.200
aubut 46,219 0.008 0.136
maxi_flyer 30,101 0.008 0.160
pasquier 20,733 0.005 0.201
nuvo 373,628 0.005 0.251
provigo_flyer 23,602 0.005 0.200
walmart_flyer 42,553 0.004 0.231
aliments_merci 216,835 0.002 0.167
saq 296,305 0.002 0.080
bocoboco 58,958 0.002 0.515
maturin 37,739 0.002 0.066
akhavan 54,497 0.001 0.200
epipresto 460,710 0.000

Interpretation: storefront feeds behind anti-bot protection (Club Entrepôt, Metro, Maxi, Super C, Provigo, Walmart) answer each anonymous session with the price of an unknown store; the resulting intra-day oscillation among 3–5 price levels is not repricing. Single-store online shops (Shopify/WooCommerce) and circulars do not show it. Consequences: weekly modal prices are the primary posted-price measure; within-banner daily range is reported as a store-unidentified lower bound on cross-store dispersion; sub-weekly volatility is not interpreted as price flexibility.

# 5. Listing-level attrition

step listings share_of_initial
All listings in snapshot (products table) 124,579 1.000
excl. SAQ (alcohol monopoly) 117,102 0.940
excl. pharmacy feeds 115,352 0.926
excl. non-food categories (health & beauty, household, pets, baby, alcohol) 112,233 0.901
with at least one observed price 111,924 0.898
excl. unrealistic prices 111,575 0.896
… with parsable package size (unit-price / matching universe) 66,340 0.533
… in a cross-banner match group (tiers A–C) 13,273 0.107

# 6. Cross-banner matching

Tiers: A = exact GS1-valid UPC (Metro ↔ Super C); B = brand + package (quantity and pack count) + identical cleaned token set + identical numeric tokens; C = brand + package + token-sort similarity ≥ 0.90 (not B); D = brand-less feed, package + identical full-name token set + branded side's brand tokens present. Multi-product circular offers ('X ou Y', 'assortis') and size-less listings are excluded from matching.

Threshold calibration on UPC ground truth (all Metro×Super C candidate pairs within brand×package blocks; positives = same UPC):

scorer thr n precision recall
sort_ratio 0.700 9,958.000 0.432 0.994
sort_ratio 0.750 8,155.000 0.527 0.992
sort_ratio 0.800 6,695.000 0.641 0.991
sort_ratio 0.850 5,601.000 0.764 0.988
sort_ratio 0.900 4,913.000 0.870 0.987
sort_ratio 0.950 4,597.000 0.928 0.986
sort_ratio 1.000 4,563.000 0.932 0.982
tokens_equal 1.000 4,563.000 0.932 0.982

Cross-banner pairs found: {'A': 4413, 'B': 4088, 'C': 714, 'D': 102}; channel (storefront↔circular, same banner) links: 1. A first version based on rapidfuzz token_set_ratio was rejected after validation (subset names scored 1.0); see match_validation_sample_v0_tokenset_rejected.csv and _v1_round1.csv.

Recall against UPC truth (4 413 Metro↔Super C pairs): tier B recovers 0.974, B+C 0.979 — an upper bound for cross-company recall because the two banners share a platform and naming conventions. Food-Ka's own product_links (2 033 pairs) are recovered at 42.5 %; the remainder are size-less or multi-offer circular links that WP37 excludes by design.

# 6.1 Validation sample (240 pairs, 60 per tier, stratified random, seed 20261101)

tier n same different unsure precision_strict ci_lo ci_hi precision_excl_unsure ci2_lo ci2_hi llm_operator_agreement upc_precision upc_recall
A 60 60 0 0 1.000 0.940 1.000 1.000 0.940 1.000 1.000 1.000
B 60 60 0 0 1.000 0.940 1.000 1.000 0.940 1.000 1.000 0.932 0.982
C 60 25 30 5 0.417 0.301 0.543 0.455 0.330 0.585 0.983 0.870 0.987
D 60 59 0 1 0.983 0.911 0.997 1.000 0.939 1.000 1.000

Labels: Claude (claude-opus-5-5) as second annotator; final label = operator review of every pair (research pipeline operator, not the PI); PI adjudication is pending and flagged in the paper. The UPC-based precision for tier B (0.913) is lower than the text-based precision because some listings with identical text carry different UPCs (packaging variants invisible in text) — both numbers are reported.

Decision for the primary specification: tiers A + B only; C and D enter sensitivity analyses.

# 7. Prospective collection plan (what cannot be done now)

  • Store geography: add a store-selection step to Loblaw/Metro/Walmart connectors (store id in the session) for a fixed panel of ~20 stores across Quebec regions, and pull Flipp circulars for ≥ 6 postal codes (Montréal, Québec, Gatineau, Sherbrooke, Saguenay, Trois-Rivières). Both are authorised, polite (one pass per store per day) and reversible changes to Food-Ka; they are not implemented in WP37 (no production change).
  • Reference prices: log regular_price and on_sale changes in price_log (one extra column) so that promotion states become directly observed.
  • Minimum duration: 26 weeks for promotion recurrence and reference-price adjustment (H3), 52 weeks for seasonal patterns and index validation against StatCan.
Files · main
Menu

Theme