1"""Recall of the name-based tiers (B/C/D) against GTIN ground truth (tier A pairs, Metro <-> Super C).23Ground truth: listing pairs sharing a GS1-valid UPC across the two Metro Inc. banners. For each truth pair we ask4whether the name/brand/size rules would have linked it without the GTIN. Also reports *false links* among GTIN-bearing5listings: name-based pairs between metro and superc whose UPCs differ (a precision check on a labelled subset).6"""7import pandas as pd, numpy as np8from rapidfuzz import fuzz9from src.common import path, get_logger10from src.matching.match_products import prepare, num_tokens1112log = get_logger("recall")1314def main():15 prod = pd.read_parquet(path("processed") / "products.parquet"); ret = pd.read_parquet(path("processed") / "retailers.parquet")16 p = prepare(prod, ret); g = p[p.gtin.notna()].copy()17 pairs = pd.read_parquet(path("processed") / "product_matches.parquet")18 truth = pairs[(pairs.tier == "A")][["uid_a", "uid_b"]]19 idx = g.set_index("uid")20 rows = []21 for ua, ub in truth.itertuples(index=False):22 a, b = idx.loc[ua], idx.loc[ub]23 brand_ok = a.bkey != "" and a.bkey == b.bkey24 size_ok = a.skey != "" and a.skey == b.skey25 sim = fuzz.token_set_ratio(a.nkey, b.nkey) / 100.026 nums_ok = num_tokens(a.nkey) == num_tokens(b.nkey)27 tier = None28 if brand_ok and size_ok and nums_ok and sim >= 0.90: tier = "B"29 elif brand_ok and size_ok and nums_ok and sim >= 0.80: tier = "C"30 elif brand_ok and a.skey == "" and b.skey == "" and nums_ok and sim >= 0.95: tier = "C"31 rows.append(dict(uid_a=ua, uid_b=ub, brand_ok=brand_ok, size_ok=size_ok, nums_ok=nums_ok, sim=sim, recovered_tier=tier,32 brand_missing=(a.bkey == "" or b.bkey == ""), size_missing=(a.skey == "" or b.skey == "")))33 r = pd.DataFrame(rows)34 log.info("GTIN truth pairs: %d ; recovered by B: %.3f ; by B+C: %.3f", len(r), (r.recovered_tier == "B").mean(), r.recovered_tier.notna().mean())35 log.info("failure reasons among non-recovered: brand missing %.3f, brand differs %.3f, size missing %.3f, size differs %.3f, numeric tokens differ %.3f, sim<0.80 %.3f",36 *(r[r.recovered_tier.isna()][c].mean() for c in ["brand_missing"]),37 ((~r.brand_ok) & (~r.brand_missing))[r.recovered_tier.isna()].mean(),38 r[r.recovered_tier.isna()].size_missing.mean(),39 ((~r.size_ok) & (~r.size_missing))[r.recovered_tier.isna()].mean(),40 (~r[r.recovered_tier.isna()].nums_ok).mean(), (r[r.recovered_tier.isna()].sim < 0.80).mean())41 r.to_csv(path("processed") / "recall_gtin_truth.csv", index=False)4243if __name__ == "__main__":44 main()45