File size: 7,008 Bytes
fc0d2a6 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 | """CPU stage: country-agnostic pair features on rule-free text views."""
import math
from multiprocessing import Pool
import numpy as np
import pandas as pd
from rapidfuzz import fuzz, process
from rapidfuzz.distance import JaroWinkler, Levenshtein
import text_utils as tu
from common import Progress, get_logger, n_workers, save_parquet, shard_path
from prep import load_prep
from retrieve_dense import block_rows, n_shards
log = get_logger("feat")
PCOLS = ["name_full", "name_core", "legal", "name_ns", "nonlatin", "addr_norm", "street", "street_ns", "loc",
"house", "nums", "postal", "alnum", "addr_empty", "src"]
PY_FEATS = ["n_soft_ab", "n_soft_ba", "n_jacc", "n_extra_a", "n_extra_b", "n_contain", "n_acro", "n_first_eq",
"legal_state", "st_soft_ab", "st_soft_ba", "loc_cov_a", "loc_cov_b", "a_jacc",
"house_state", "house_diff", "num_jacc", "num_state", "postal_state", "alnum_state",
"n_tok_a", "n_tok_b", "a_tok_a", "a_tok_b"]
FUZZ = [("n_ratio", "name_core", fuzz.ratio), ("n_jw", "name_core", JaroWinkler.normalized_similarity),
("n_lev", "name_core", Levenshtein.normalized_similarity), ("n_tsort", "name_core", fuzz.token_sort_ratio),
("n_tset", "name_core", fuzz.token_set_ratio), ("n_partial", "name_core", fuzz.partial_ratio),
("n_full_ratio", "name_full", fuzz.ratio), ("ns_ratio", "name_ns", fuzz.ratio),
("ns_partial", "name_ns", fuzz.partial_ratio), ("a_ratio", "addr_norm", fuzz.ratio),
("a_tset", "addr_norm", fuzz.token_set_ratio), ("a_partial", "addr_norm", fuzz.partial_ratio),
("st_ratio", "street", fuzz.ratio), ("st_ns_ratio", "street_ns", fuzz.ratio)]
def _jacc(a, b):
if not a and not b:
return math.nan
return len(a & b) / len(a | b)
def _py_chunk(args):
A, B = args # dicts of column -> list for the query side and candidate side
out = np.empty((len(A["name_core"]), len(PY_FEATS)), dtype=np.float32)
for i in range(len(out)):
na, nb = A["name_core"][i].split(), B["name_core"][i].split()
sa, sb = set(na), set(nb)
fa, fb = A["name_full"][i].split(), B["name_full"][i].split()
n_ab, n_ba = tu.soft_cov(na, nb), tu.soft_cov(nb, na)
extra_a = sum(1 for x in na if x not in sb and not any(tu.tok_sim(x, y) > 0 for y in nb))
extra_b = sum(1 for x in nb if x not in sa and not any(tu.tok_sim(x, y) > 0 for y in na))
contain = 1.0 if tu.contains_seq(na, fb) else (2.0 if tu.contains_seq(nb, fa) else 0.0)
acro = 1.0 if (tu.acronym_match(na, nb) or tu.acronym_match(nb, na)) else 0.0
first = 1.0 if na and nb and na[0] == nb[0] else 0.0
leg = tu.legal_state(A["legal"][i], B["legal"][i])
sta, stb = A["street"][i].split(), B["street"][i].split()
st_ab, st_ba = tu.soft_cov(sta, stb), tu.soft_cov(stb, sta)
ta, tb = A["addr_norm"][i].split(), B["addr_norm"][i].split()
la = [t for seg in A["loc"][i].split("|") for t in seg.split() if not t.isdigit()]
lb = [t for seg in B["loc"][i].split("|") for t in seg.split() if not t.isdigit()]
loc_a = tu.soft_cov(la, tb) # does the query's city/region appear anywhere in the candidate
loc_b = tu.soft_cov(lb, ta)
hs, hd = tu.house_compare(A["house"][i], B["house"][i])
ma, mb = set(A["nums"][i].split()), set(B["nums"][i].split())
pa, pb = set(A["postal"][i].split()), set(B["postal"][i].split())
xa, xb = set(A["alnum"][i].split()), set(B["alnum"][i].split())
out[i] = (n_ab, n_ba, _jacc(sa, sb), extra_a, extra_b, contain, acro, first, leg, st_ab, st_ba,
loc_a, loc_b, _jacc(set(ta), set(tb)), hs, hd, _jacc(ma, mb), tu.set_state(ma, mb),
tu.set_state(pa, pb), tu.set_state(xa, xb), len(na), len(nb), len(ta), len(tb))
return out
def _gather(tab, idx, cols):
return {c: tab[c][idx].tolist() for c in cols}
def pair_features(pool, s1tab, pooltab, df):
q, c = df.q.values, df.c.values
feats = {}
for name, col, scorer in FUZZ:
a, b = s1tab[col][q].tolist(), pooltab[col][c].tolist() # lists: ~10x faster than object arrays
heavy = scorer in (fuzz.partial_ratio, fuzz.token_set_ratio, fuzz.token_sort_ratio)
v = process.cpdist(a, b, scorer=scorer, workers=16 if heavy else 1).astype(np.float32)
empty = (pd.Series(a).str.len().values == 0) | (pd.Series(b).str.len().values == 0)
v[empty] = np.nan
feats[name] = v
str_cols = ["name_core", "name_full", "legal", "street", "addr_norm", "loc", "house", "nums", "postal", "alnum"]
step = 20_000
args = [(_gather(s1tab, q[i:i + step], str_cols), _gather(pooltab, c[i:i + step], str_cols))
for i in range(0, len(df), step)]
py = np.concatenate(list(pool.imap(_py_chunk, args))) if args else np.empty((0, len(PY_FEATS)), np.float32)
for j, name in enumerate(PY_FEATS):
feats[name] = py[:, j]
feats["nonlatin_a"] = s1tab["nonlatin"][q].astype(np.float32)
feats["nonlatin_b"] = pooltab["nonlatin"][c].astype(np.float32)
feats["addr_empty_a"] = s1tab["addr_empty"][q].astype(np.float32)
feats["addr_empty_b"] = pooltab["addr_empty"][c].astype(np.float32)
feats["cand_src"] = pooltab["src"][c].astype(np.float32)
return pd.DataFrame(feats)
def query_context(df):
g = df.groupby("q")
out = pd.DataFrame(index=df.index)
out["q_ncand"] = g.c.transform("size").astype(np.float32)
for m in ("e5", "bge"):
best = g[f"{m}_cos"].transform("max")
out[f"{m}_cos_gap"] = (best - df[f"{m}_cos"]).astype(np.float32)
out[f"q_best_{m}"] = best.astype(np.float32)
out[f"{m}_cos_rank"] = g[f"{m}_cos"].rank(ascending=False, method="first").astype(np.float32)
out["q_best_n_tset"] = g.n_tset.transform("max").astype(np.float32)
return out
def run_features(split):
ns = n_shards(split)
todo = [i for i in range(ns) if not shard_path("feat", split, i).exists()]
if not todo:
return
with Pool(n_workers()) as pool: # fork before big tables are loaded
s1 = load_prep(split, "s1", [c for c in PCOLS if c != "src"])
pl = load_prep(split, "pool", PCOLS)
s1tab = {c: s1[c].values for c in s1.columns}
pooltab = {c: pl[c].values for c in pl.columns}
del s1, pl
prog = Progress(log, len(todo), f"features {split} shards")
for i in todo:
df = block_rows(split, i)
for m in ("e5", "bge"):
z = np.load(shard_path("cos", split, i, "npz", m))
for k in z.files:
df[k] = z[k]
f = pair_features(pool, s1tab, pooltab, df)
df = pd.concat([df, f], axis=1)
df = pd.concat([df, query_context(df)], axis=1)
save_parquet(df, shard_path("feat", split, i))
prog.step()
def feature_columns(df):
return [c for c in df.columns if c not in ("q", "c", "y")]
|