PoPTard5 / model-3 /src /decide.py
Jyo-K's picture
Upload folder using huggingface_hub
e79d917 verified
Raw History Blame Contribute Delete
2.97 kB
"""Decision layer: calibration, conflict resolution, per-entity expected-F0.5 selection."""
import numpy as np
import pandas as pd
from sklearn.isotonic import IsotonicRegression
def fit_calibrator(p, y):
iso = IsotonicRegression(out_of_bounds="clip", y_min=1e-4, y_max=1 - 1e-4)
iso.fit(p, y)
return iso
def resolve_conflicts(df, prob_col="p"):
"""Each S2/S3 record may belong to at most one S1 entity: keep only its best-scoring
claim (applied before selection, so a record never counts for two entities).
TIES MUST BE BROKEN. The previous version kept every row with `p >= max(p)` for a
record, which silently kept ALL tied claimants. Ties are not rare here: isotonic
calibration is piecewise-constant, so many distinct raw scores collapse onto the
same calibrated probability. On the full test set that let S3-709463074 be claimed
by two entities at once - caught by the one-owner assert in run_test.py.
Sorting by (c, -p) with a stable kind and keeping the first row per record gives
exactly one owner, deterministically, with the original row order breaking ties."""
d = df.sort_values(["c", prob_col], ascending=[True, False], kind="mergesort")
return d[~d.duplicated(subset="c", keep="first")]
def expected_f05_select(probs, miss_mass, floor=0.0, beta=0.5):
"""probs: calibrated probabilities for one entity's candidates (any order).
Returns indices (into probs) of the chosen set that maximises expected F-beta."""
b2 = beta * beta
o = np.argsort(-probs)
ps = probs[o]
total = ps.sum() + miss_mass
best_k = 0
best = float(np.prod(1 - ps) * np.exp(-miss_mass)) # P(entity is a singleton)
s = 0.0
for k in range(1, len(ps) + 1):
if ps[k - 1] < floor:
break
s += ps[k - 1]
ef = (1 + b2) * s / ((1 + b2) * s + b2 * (total - s) + (k - s))
if ef > best:
best, best_k = ef, k
return o[:best_k]
def predict_sets(df, id_col, cand_col, prob_col, method, param, miss_mass=0.0, conflicts=True):
"""method: 'threshold' (param = threshold) or 'ef05' (param = probability floor)."""
d = resolve_conflicts(df, prob_col) if conflicts else df
out = {}
if method == "threshold":
d = d[d[prob_col] >= param]
for q, g in d.groupby(id_col):
out[q] = list(g[cand_col].values)
return out
for q, g in d.groupby(id_col):
idx = expected_f05_select(g[prob_col].values, miss_mass, floor=param)
if len(idx):
out[q] = list(g[cand_col].values[idx])
return out
def tune(df, truths, ids, id_col, cand_col, prob_col, method, grid, miss_mass, conflicts, scorer):
best = (-1, None)
for v in grid:
m, _ = scorer(predict_sets(df, id_col, cand_col, prob_col, method, v, miss_mass, conflicts),
truths, ids)
if m["f05"] > best[0]:
best = (m["f05"], v)
return best[1], best[0]