PoPTard5 / model-3 /scripts /empty_addr.py
Jyo-K's picture
Upload folder using huggingface_hub
e79d917 verified
Raw History Blame Contribute Delete
5.19 kB
"""Are our NEVER-RETRIEVED true pairs concentrated in empty-address records?
LIFTOVER R4 reports, for bge-out: 49% of retrieval misses were empty-address candidates, and only
52% of those were ever retrieved [M]. And 344,883 pool records (3.34%) genuinely have an empty name
or address on the RAW TSVs.
That matters because our miss decomposition is:
missed true pairs 2,974
never retrieved 1,211 (40.7%) <- the ONLY loss that is not ambiguity
retrieved but rejected 1,763
and the never-retrieved group is the one remaining lever that a better scorer cannot touch. If it
is concentrated in empty-address records, a name-only index over just those 345K records is a
targeted, ~5-minute CPU fix rather than a full retrieval rebuild.
If it is NOT concentrated there, R4 is a bge-out-specific artefact and model-3 should skip it.
"""
import json
import os
import sys
import joblib
import numpy as np
import pandas as pd
sys.path.insert(0, "src")
import config as C
import decide as DC
from evaluate import macro_f05
RAW = os.environ.get("RAW_DIR", "/scratch/user4/dai/dataset_unstop/dataset/train")
b = joblib.load("work/results_A/model_bundle.joblib")
MM, THR = b["miss_mass"], b["param"]
CACHE = "work/val_scored_20000.parquet"
cand = pd.read_parquet(CACHE)
truths = {int(k): set(v) for k, v in json.load(open(CACHE + ".truth.json")).items()}
ids = sorted(truths)
shipped = DC.predict_sets(cand, "q", "c", "p", b["method"], THR, MM, True)
p = C.PATHS["train"]
s2 = pd.read_parquet(p["source2"], columns=["entity_id"])
s3 = pd.read_parquet(p["source3"], columns=["entity_id"])
n_s2 = len(s2)
pool_ids = np.concatenate([s2["entity_id"].values, s3["entity_id"].values])
raw = {}
for src in (2, 3):
d = pd.read_csv(os.path.join(RAW, f"train_source{src}.tsv"), sep="\t", dtype=str,
keep_default_na=False, na_filter=False)
for e, n, a in zip(d["entity_id"], d["business_name"], d["business_address"]):
raw[e] = (n, a)
empty_addr = np.zeros(len(pool_ids), bool)
empty_name = np.zeros(len(pool_ids), bool)
for i, e in enumerate(pool_ids):
n, a = raw[e]
empty_addr[i] = not a.strip()
empty_name[i] = not n.strip()
print(f"pool {len(pool_ids):,}")
print(f" empty ADDRESS: {int(empty_addr.sum()):>9,} ({empty_addr.mean():.3%})")
print(f" empty NAME : {int(empty_name.sum()):>9,} ({empty_name.mean():.4%})")
cand_by_q = {int(q): set(g["c"].tolist()) for q, g in cand.groupby("q", sort=False)}
never, rejected, hit = [], [], []
for q in ids:
t = truths.get(q, set())
if not t:
continue
cs = cand_by_q.get(q, set())
pred = set(shipped.get(q, []))
never += list(t - cs)
rejected += list((t & cs) - pred)
hit += list(t & pred)
print(f"\ntrue pairs: retrieved+predicted {len(hit):,} "
f"retrieved+rejected {len(rejected):,} never retrieved {len(never):,}")
print("\n" + "=" * 74)
print("EMPTY-ADDRESS RATE BY OUTCOME (the test of LIFTOVER R4)")
print("=" * 74)
print(f"{'group':<26}{'n':>9}{'empty addr':>13}{'empty name':>13}")
for name, grp in (("predicted correctly", hit), ("retrieved but rejected", rejected),
("NEVER RETRIEVED", never)):
if not grp:
continue
a = np.array(grp)
print(f"{name:<26}{len(a):>9,}{empty_addr[a].mean():>12.2%}{empty_name[a].mean():>13.3%}")
base = empty_addr.mean()
print(f"{'pool base rate':<26}{len(pool_ids):>9,}{base:>12.2%}")
a = np.array(never)
if len(a):
lift = empty_addr[a].mean() / max(base, 1e-9)
print(f"\n never-retrieved empty-address enrichment vs pool: {lift:.2f}x")
share = empty_addr[a].mean()
print(f" share of never-retrieved misses that are empty-address: {share:.2%}")
print(f" LIFTOVER reports 49% for bge-out")
if share > 0.25:
recover = len(a) * share
print(f"\n => a name-only index over the {int(empty_addr.sum()):,} empty-address pool")
print(f" records could reach up to {recover:,.0f} of our {len(a):,} never-retrieved")
print(f" pairs. At the measured ~0.0507 F0.5 per recovered TP and 20,000 entities,")
print(f" the CEILING is {recover*0.0507/len(ids):+.5f} - and only if every one is")
print(f" both retrieved AND then accepted by the scorer.")
else:
print("\n => NOT concentrated in empty-address records. R4 is a bge-out artefact;")
print(" model-3 should skip it.")
print("\n" + "=" * 74)
print("WHAT DO THE NEVER-RETRIEVED PAIRS LOOK LIKE? (10 samples)")
print("=" * 74)
s1raw = pd.read_csv(os.path.join(RAW, "train_source1.tsv"), sep="\t", dtype=str,
keep_default_na=False, na_filter=False)
s1p = pd.read_parquet(p["source1"], columns=["entity_id"])
s1_ids = s1p["entity_id"].values
s1map = dict(zip(s1raw["entity_id"], zip(s1raw["business_name"], s1raw["business_address"])))
shown = 0
for q in ids:
t = truths.get(q, set())
cs = cand_by_q.get(q, set())
for c in (t - cs):
qn, qa = s1map[s1_ids[q]]
cn, ca = raw[pool_ids[c]]
print(f" S1: {qn[:46]!r:<50} | {qa[:40]!r}")
print(f" ->: {cn[:46]!r:<50} | {ca[:40]!r}")
shown += 1
break
if shown >= 10:
break