PoPTard5 / model-3 /src /data.py
Jyo-K's picture
Upload folder using huggingface_hub
e79d917 verified
Raw History Blame Contribute Delete
4.05 kB
"""Loading, S1 augmentation with ground truth, sanity checks and splits.
Unchanged from model-2 except: load_data() takes an explicit paths dict so the
same code serves train and test, and test loading needs no ground truth.
"""
import os
import numpy as np
import pandas as pd
S1_COLS = ["entity_id", "business_name", "business_address", "country", "country_norm",
"name_norm", "name_core", "legal_suffix", "address_abbrev_norm", "address_numbers"]
S23_COLS = S1_COLS
def read_any(path, columns=None):
if path.endswith(".parquet"):
df = pd.read_parquet(path, columns=columns)
else:
df = pd.read_csv(path, sep="\t", dtype=str, keep_default_na=False, usecols=columns)
for c in df.columns:
df[c] = df[c].fillna("").astype(str)
return df
def split_ids(s):
return [x for x in str(s).split(",") if x]
def load_data(paths, with_gt=True):
s1 = read_any(paths["source1"], S1_COLS)
s2 = read_any(paths["source2"], S23_COLS)
s3 = read_any(paths["source3"], S23_COLS)
s23 = pd.concat([s2, s3], ignore_index=True)
if not with_gt:
return s1, s23, None
gt = read_any(paths["ground_truth"])
return s1, s23, gt
def augment_s1(s1, gt, out_path=None):
"""Attach ground-truth labels to Source 1 as column `matched_entity_ids`."""
assert s1["entity_id"].is_unique, "duplicate entity_id in source1"
assert gt["source1_entity_id"].is_unique, "duplicate source1_entity_id in ground truth"
s1 = s1.merge(gt.rename(columns={"source1_entity_id": "entity_id"}), on="entity_id",
how="left", validate="one_to_one")
missing = (s1["matched_entity_ids"].isna()).sum()
assert missing == 0, f"{missing} source1 entities have no ground-truth row"
s1["matched_entity_ids"] = s1["matched_entity_ids"].fillna("").astype(str)
if out_path:
s1.to_parquet(out_path, index=False)
return s1
def sanity_checks(s1, s23, rng, sample=200_000):
rep = {}
rep["n_s1"] = len(s1)
rep["n_s23"] = len(s23)
rep["s23_ids_unique"] = bool(s23["entity_id"].is_unique)
lists = s1["matched_entity_ids"].map(split_ids)
n = lists.map(len)
rep["singleton_rate"] = float((n == 0).mean())
rep["mean_matches"] = float(n.mean())
rep["max_matches"] = int(n.max())
flat = pd.Series([x for l in lists for x in l])
rep["n_matched_ids"] = int(len(flat))
rep["multi_owner_ids"] = int(flat.duplicated().sum())
idx = pd.Index(s23["entity_id"])
pos = idx.get_indexer(flat)
rep["gt_ids_missing_in_s23"] = int((pos < 0).sum())
# same-country rate over a sample of true pairs
owner = np.repeat(np.arange(len(s1)), n.values)
take = rng.choice(len(flat), size=min(sample, len(flat)), replace=False)
ok = pos[take] >= 0
q_c = s1["country_norm"].values[owner[take][ok]]
c_c = s23["country_norm"].values[pos[take][ok]]
rep["true_pairs_same_country_rate"] = float((q_c == c_c).mean()) if ok.any() else None
rep["s1_country_counts"] = s1["country_norm"].value_counts().to_dict()
return rep
def make_split(s1, cfg, rng):
"""Return three disjoint arrays of S1 row positions: ce, stack, val."""
n_ce, n_st, n_va = cfg["n_s1_ce"], cfg["n_s1_stack"], cfg["n_s1_val"]
if cfg["split"] == "A":
perm = rng.permutation(len(s1))
need = n_ce + n_st + n_va
if need > len(perm):
raise ValueError(f"requested {need} S1 entities but only {len(perm)} exist")
return perm[:n_ce], perm[n_ce:n_ce + n_st], perm[n_ce + n_st:need]
if cfg["split"] == "B":
c = s1["country_norm"].values
tr = rng.permutation(np.where(c == cfg["split_b_train_country"])[0])
va = rng.permutation(np.where(c == cfg["split_b_val_country"])[0])
if n_ce + n_st > len(tr) or n_va > len(va):
raise ValueError("not enough entities for split B with these sizes")
return tr[:n_ce], tr[n_ce:n_ce + n_st], va[:n_va]
raise ValueError(cfg["split"])
def ensure_dir(p):
os.makedirs(p, exist_ok=True)
return p