A scraper's README usually ends with a field list: price, unitPrice, ean, inStock, and so on. It tells you what a row can hold. It does not tell you how often the field is filled, whether a join between two shops lands on the same product, or whether last Tuesday's run worked at all. Those are the questions that decide whether the data is usable, and a field list cannot answer them.
So for the scrapers I run on Apify (UK supermarkets, Australian supermarkets, Google Hotels prices and UK property listings) I published a data-quality page with five measures per data source, each with its sample and date. This post explains what each measure catches and what ours show, including the parts that are not flattering.
The five measures
1. Identifier coverage. The share of rows that carry a barcode (EAN/GTIN), or for sources with no barcode, the identifier that plays the same role. It catches the difference between "the schema has an ean field" and "you can actually join on it". In the 4 October 2026 weekly snapshot, Tesco rows carried a barcode on 99.1% of 23,352 rows, Sainsbury's on 100.0% of 26,511 and Asda on 98.9% of 20,196. Aldi and Morrisons publish no barcodes at all, and the page says so rather than leaving a blank. For hotels the identifier is Google's hotel id, which every row carries by construction, so the page says that is not a measured figure. For property it is the UPRN, which only one portal gives.
2. Cross-retailer match accuracy, with its method. If you join the same product across shops, how often is the join right? Ours is measured on every pair of rows from two different UK retailers with an identical matchKey (a normalised brand, name and pack size) in the same 4 October snapshot: 4,251 pairs. Where both rows carry a barcode (4,225 pairs), the pair is scored by whether the barcodes agree: 4,124 do, 97.6%. That is conservative, because a product listed under a second barcode at one shop counts as wrong. The 127 pairs a barcode cannot settle were read by hand on brand, name, pack size and price; counting those, 4,228 of 4,251 are the same product, 99.5%.
The caveat matters as much as the number: exact matchKey joins find only about 14.8% of the pairs that a barcode proves are the same product. Shops word the same product differently. So the key is precise but finds few matches, and an accuracy figure without that caveat would mislead anyone planning a join on it.
3. Run success. The share of scheduled canary runs that succeeded over a stated window. For UK grocery that is 36 runs from 25 September to 13 October 2026 from two daily canaries, all 36 marked SUCCEEDED. Two things about that definition are stated on the page. A run counts as succeeded when the platform marks it SUCCEEDED, so warnings inside a succeeded run are not counted as failures. And a canary is a small smoke test: it shows that a run can reach its sources, not that every field is complete. The hotels canary shows what a real failure looks like in this measure: 13 of 14 runs (92.9%), with the 1 October run failing on a place lookup, fixed that week.
4. Per-field completeness. The share of rows with a value for each field that matters. For the 73,745 UK rows in the 4 October snapshot, shelf price is 100.0%, unit price 99.6% and pack size 89.9%. The per-retailer split is where it gets useful: pack size is 100.0% for Asda but 84.1% for Tesco and 86.9% for Sainsbury's, and Aldi's unit price is 94.1%. An overall figure hides which shop is short.
5. Known gaps, stated plainly. The limits a buyer would otherwise find the hard way, in one list per source.
What the numbers turned up
Writing the page down forced some findings into the open.
- Aldi rows mostly cannot be joined automatically. Aldi publishes no barcodes and no stock status, and its products rarely share an exact name with another retailer's. The page's advice is to compare Aldi by unit price instead.
- Woolworths stock is always true. In a 7,194-row Woolworths sample (store 1101, 7 October 2026), stock status is present on 100% of rows, but every value is true, because the listing shows only available products. A completeness figure alone would have called that field perfect.
- Zoopla's UPRN depends on the listing type. With details on, 47% of rows (71 of 150) carried a UPRN in a London for-sale sample of 24 September 2026 that included 67 new-homes listings, against 70% (7 of 10) in a separate 10-listing sample with new homes excluded. Both are small, dated samples, and the page says so; the known gap is that UPRN and EPC rating are often missing on new-homes listings.
-
The hotels
bookingUrlis null by design. Listing it as a known gap is cheaper than answering the same question from every new user. - Morrisons has no barcode measure, because it publishes none. The page shows that as a stated "None", not a missing number.
How the numbers are produced
Three sources, no estimates:
- Weekly snapshot files. For UK grocery, barcode coverage, completeness and match accuracy are counted from the latest complete weekly snapshot, one row per product per retailer, with the week and date stated next to each figure.
- Canary run metadata. Run success is read from the scheduled canaries' run records: the platform status, the dates and the count, nothing inferred.
- Hand checks. Where a measure needed a person, as with the 127 match pairs a barcode could not settle, the sample size and the method are stated with the figure.
Every figure is dated, and where there is no measurement the row is left out rather than filled with a guess. The page carries its last-updated date at the top (13 October 2026 for the figures here); each grocery figure names the weekly snapshot it came from, so check both before quoting one.
If you want to see the raw material, the free daily UK basket is on Kaggle with a first-look notebook, and the scraper itself is the UK Supermarket Price Scraper on Apify.
Reading anyone's quality claims
When a data vendor, including me, tells you the data is accurate, ask four things: accurate on what sample, measured how, as of when, and what does the method miss? A match rate with no recall figure, a success rate with no definition of success, or a completeness figure with no per-source split is a number you cannot use yet. The useful version is usually less impressive and much more helpful.
Prices are online shelf prices as published by each retailer on the day shown; they are not store-specific unless stated. This article is not affiliated with, endorsed by, or partnered with any of the retailers, portals or sites named; their names are used only to describe public data, and product names, images and descriptions remain theirs.
Top comments (2)
The 14.8% recall caveat and the always-true stock example make these numbers much more usable. I'd add a stale-but-successful canary fixture: the fetch succeeds and all required fields are present, but it returns yesterday's cached shelf prices. Would the quality page distinguish platform SUCCEEDED from freshness, using source observation time and snapshot age? That would catch a run that is operationally green while no longer answering "what does this cost today?", without changing the honest success-rate definition you've already stated.
Good distinction, and no — the page doesn't measure freshness yet, only platform status, and it says so. What we have today: every row carries the run's own start time (scrapedAt), and each run reads the retailer's live pages at that moment, with no cache layer of ours in between, so the residual case is the retailer serving a stale page to everyone. A "no prices changed since yesterday" alarm doesn't work for a 212-item basket (most days most prices don't move), so the signal would have to be something like promotion end dates that have already passed, or a second independent read. I'll add "freshness is not measured" to the known gaps rather than imply it is, and think about a fixture along the lines you describe. Thanks — this is the kind of comment the page was written to get.