Download model-3/scripts/deep_analysis.py from Jyo-K/PoPTard5: direct link, hf CLI and curl.
- Browser
- Download file 7.76 kB
-
https://huggingface.co/Jyo-K/PoPTard5/resolve/main/model-3/scripts/deep_analysis.py
- Command line
-
hf download hf://Jyo-K/PoPTard5/model-3/scripts/deep_analysis.py
-
curl -L -o deep_analysis.py https://huggingface.co/Jyo-K/PoPTard5/resolve/main/model-3/scripts/deep_analysis.py
7.76 kB
| """Where did our F0.5 actually go, and what is recoverable? | |
| Uses the cached scored val set (calibrated probabilities already computed), so every | |
| number here is measured, not modelled. Answers: | |
| 1. ORACLE - best possible F0.5 given the candidates we retrieved (the real headroom) | |
| 2. DECOMPOSITION - of the true pairs we miss, how many were never retrieved vs | |
| retrieved-and-rejected vs lost to one-owner arbitration | |
| 3. OPERATING POINT - F0.5 / precision / recall across the threshold sweep; is there a | |
| better precision-recall trade than the one we shipped? | |
| 4. EXACT EXPECTED-F0.5 vs our plug-in approximation. | |
| F0.5 = 1.25*TP / (1.25TP + 0.25FN + FP). With FP = k - TP and FN = T - TP the TP | |
| terms cancel in the denominator: F0.5 = 1.25*TP / (0.25*T + k) | |
| Our decide.py evaluates that at E[TP] and E[T] - a plug-in. The exact value is | |
| E[ 1.25*TP / (0.25*(TP + U + M) + k) ] where TP ~ PoissonBinomial(selected probs), | |
| U ~ PoissonBinomial(unselected probs), M = expected unretrieved true matches. | |
| Computed here with two Poisson-binomial DPs per entity. | |
| """ | |
| import json, sys, time | |
| import numpy as np | |
| import pandas as pd | |
| import joblib | |
| sys.path.insert(0, "src") | |
| import decide as DC | |
| from evaluate import macro_f05, entity_scores | |
| ART = "work/results_A" | |
| CACHE = "work/val_scored_20000.parquet" | |
| t0 = time.time() | |
| log = lambda m: print(f"[{time.time()-t0:5.0f}s] {m}", flush=True) | |
| b = joblib.load(f"{ART}/model_bundle.joblib") | |
| miss_mass = b["miss_mass"] | |
| cand = pd.read_parquet(CACHE) | |
| truths = {int(k): set(v) for k, v in json.load(open(CACHE + ".truth.json")).items()} | |
| ids = sorted(truths) | |
| log(f"{len(ids):,} val entities, {len(cand):,} scored candidate pairs") | |
| log(f"shipped rule: {b['method']}({b['param']}) miss_mass={miss_mass:.4f}") | |
| by_q = {q: g for q, g in cand.groupby("q")} | |
| ctry = {q: g["ctry"].iloc[0] for q, g in by_q.items()} | |
| # ---------------------------------------------------------------- 1. ORACLE | |
| n_true_total = sum(len(truths[q]) for q in ids) | |
| retr, oracle_preds = 0, {} | |
| for q in ids: | |
| g = by_q.get(q) | |
| cs = set(g["c"].values.tolist()) if g is not None else set() | |
| hit = truths[q] & cs | |
| retr += len(hit) | |
| if hit: | |
| oracle_preds[q] = list(hit) | |
| m_or, _ = macro_f05(oracle_preds, truths, ids) | |
| log("") | |
| print("=" * 78) | |
| print("1. HEADROOM") | |
| print("=" * 78) | |
| print(f" true pairs total : {n_true_total:,}") | |
| print(f" retrieved by blocking : {retr:,} ({retr/n_true_total:.4f} pair recall)") | |
| print(f" ORACLE F0.5 on our candidates : {m_or['f05']:.4f} <- ceiling for the scorer") | |
| print(f" perfect blocking + perfect scorer: 1.0000") | |
| # ---------------------------------------------------------------- current | |
| shipped = DC.predict_sets(cand, "q", "c", "p", b["method"], b["param"], miss_mass, True) | |
| m_now, per_now = macro_f05(shipped, truths, ids) | |
| print(f" SHIPPED F0.5 : {m_now['f05']:.4f} " | |
| f"(P {m_now['precision']:.4f} R {m_now['recall']:.4f})") | |
| print(f" gap to oracle : {m_or['f05']-m_now['f05']:.4f} <- scorer/decision loss") | |
| print(f" gap oracle to 1.0 : {1-m_or['f05']:.4f} <- blocking loss") | |
| # ---------------------------------------------------------------- 2. DECOMPOSITION | |
| never = rejected = 0 | |
| fp = 0 | |
| for q in ids: | |
| g = by_q.get(q) | |
| cs = set(g["c"].values.tolist()) if g is not None else set() | |
| pred = set(shipped.get(q, [])) | |
| t = truths[q] | |
| never += len(t - cs) | |
| rejected += len((t & cs) - pred) | |
| fp += len(pred - t) | |
| print() | |
| print("=" * 78) | |
| print("2. WHERE THE MISSES ARE") | |
| print("=" * 78) | |
| miss = never + rejected | |
| print(f" missed true pairs : {miss:,}") | |
| print(f" never retrieved (blocking) : {never:,} ({never/max(miss,1):.1%})") | |
| print(f" retrieved but rejected (scorer): {rejected:,} ({rejected/max(miss,1):.1%})") | |
| print(f" false positives predicted : {fp:,}") | |
| # ---------------------------------------------------------------- 3. OPERATING POINT | |
| print() | |
| print("=" * 78) | |
| print("3. OPERATING POINT (is precision over-weighted in practice?)") | |
| print("=" * 78) | |
| print(f"{'threshold':>10} {'F0.5':>9} {'precision':>10} {'recall':>9} {'avg preds':>10}") | |
| best = (0, None) | |
| for thr in np.round(np.arange(0.30, 0.96, 0.05), 2): | |
| p = DC.predict_sets(cand, "q", "c", "p", "threshold", thr, miss_mass, True) | |
| m, _ = macro_f05(p, truths, ids) | |
| n_pred = np.mean([len(p.get(q, [])) for q in ids]) | |
| star = "" | |
| if m["f05"] > best[0]: | |
| best = (m["f05"], thr); star = "" | |
| print(f"{thr:>10.2f} {m['f05']:>9.5f} {m['precision']:>10.4f} {m['recall']:>9.4f} {n_pred:>10.2f}") | |
| print(f" best threshold {best[1]} -> {best[0]:.5f}") | |
| # ------------------------------------------------- 4. EXACT EXPECTED F0.5 (DP) | |
| def pb_dist(ps): | |
| """Poisson-binomial pmf over sum of independent Bernoulli(ps).""" | |
| d = np.zeros(len(ps) + 1); d[0] = 1.0 | |
| for p in ps: | |
| d[1:] = d[1:] * (1 - p) + d[:-1] * p | |
| d[0] *= (1 - p) | |
| return d | |
| def exact_select(probs, M, kmax=None): | |
| """Choose k maximising the EXACT E[F0.5] = E[1.25*TP / (0.25*(TP+U+M) + k)].""" | |
| o = np.argsort(-probs) | |
| ps = probs[o] | |
| n = len(ps) | |
| kmax = n if kmax is None else min(kmax, n) | |
| # suffix Poisson-binomial for the unselected tail, built incrementally | |
| best_k, best_v = 0, 0.0 | |
| # k = 0 -> F0.5 = 0 unless the entity truly has no matches; the empty set scores | |
| # 1.0 only when T == 0, whose probability is P(all candidates false) * P(M==0). | |
| tail = pb_dist(ps) # all unselected when k = 0 | |
| p_T0 = tail[0] * (1.0 if M <= 0 else np.exp(-M)) | |
| best_v = p_T0 # E[F0.5 | k=0] = P(T = 0) * 1 | |
| head = np.array([1.0]) | |
| for k in range(1, kmax + 1): | |
| head = np.convolve(head, [1 - ps[k - 1], ps[k - 1]]) | |
| tail = pb_dist(ps[k:]) if k < n else np.array([1.0]) | |
| v = 0.0 | |
| for tp in range(len(head)): | |
| if head[tp] <= 1e-12: | |
| continue | |
| for u in range(len(tail)): | |
| if tail[u] <= 1e-12: | |
| continue | |
| denom = 0.25 * (tp + u + M) + k | |
| if denom > 0: | |
| v += head[tp] * tail[u] * (1.25 * tp / denom) | |
| if v > best_v: | |
| best_v, best_k = v, k | |
| return o[:best_k] | |
| print() | |
| print("=" * 78) | |
| print("4. EXACT EXPECTED-F0.5 vs OUR PLUG-IN") | |
| print("=" * 78) | |
| d = DC.resolve_conflicts(cand, "p") | |
| preds_exact = {} | |
| t1 = time.time() | |
| for q, g in d.groupby("q"): | |
| pr = g["p"].values | |
| if len(pr) > 30: | |
| keep = np.argsort(-pr)[:30] | |
| g = g.iloc[keep]; pr = g["p"].values | |
| idx = exact_select(pr, miss_mass) | |
| if len(idx): | |
| preds_exact[q] = list(g["c"].values[idx]) | |
| m_ex, _ = macro_f05(preds_exact, truths, ids) | |
| log(f"exact DP over {len(ids):,} entities took {time.time()-t1:.0f}s") | |
| m_ef, _ = macro_f05(DC.predict_sets(cand, "q", "c", "p", "ef05", 0.0, miss_mass, True), truths, ids) | |
| print(f" plug-in ef05(0.0) F0.5 {m_ef['f05']:.5f} P {m_ef['precision']:.4f} R {m_ef['recall']:.4f}") | |
| print(f" shipped threshold F0.5 {m_now['f05']:.5f} P {m_now['precision']:.4f} R {m_now['recall']:.4f}") | |
| print(f" EXACT DP F0.5 {m_ex['f05']:.5f} P {m_ex['precision']:.4f} R {m_ex['recall']:.4f}") | |
| print(f" exact vs shipped {m_ex['f05']-m_now['f05']:+.5f}") | |
| # ---------------------------------------------------------------- 5. BY COUNTRY | |
| print() | |
| print("=" * 78) | |
| print("5. BY COUNTRY (shipped rule)") | |
| print("=" * 78) | |
| for c in sorted(set(ctry.values())): | |
| sub = [q for q in ids if ctry.get(q) == c] | |
| m, _ = macro_f05({q: shipped.get(q, []) for q in sub}, truths, sub) | |
| print(f" {c:<8} n={len(sub):>6} F0.5 {m['f05']:.4f} P {m['precision']:.4f} R {m['recall']:.4f}") | |