Skip to content

Commit b6aade2

Browse files
committed
testing
Fixed _N like fast_testing figure 2 random tests + CI band
1 parent 3f47d69 commit b6aade2

2 files changed

Lines changed: 878 additions & 220 deletions

File tree

‎UnitMatchPy/PaperAnalyses/figure2.ipynb‎

Lines changed: 799 additions & 192 deletions
Large diffs are not rendered by default.

‎UnitMatchPy/PaperAnalyses/plot_auc_vs_delta_days_fixed_n.py‎

Lines changed: 79 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,23 @@
1111
# below) and applied per ΔDay bin like plot_auc_vs_delta_days.py: for every
1212
# session pair, every model's positive set is *re-derived* as the top N
1313
# candidate pairs by that model's own "UM Probabilities" score, where N is
14-
# REFERENCE_MODEL's own match count for that same pair (an unordered-unit-
15-
# pair count, not identity -- see _build_reference_n_lookup()). This mirrors
16-
# fast_testing.py's get_matches_1model(fixed_n=...) closely: the ranking
17-
# operates over the *full* across-session candidate pool for that pair, not
18-
# just the pipeline's own already-decided matches, so a model can both lose
19-
# matches it had decided on (if they rank low) and gain ones it hadn't (if
20-
# they rank in the top N despite being below that model's own decision
21-
# threshold) -- unlike an earlier, more conservative version of this script
22-
# that only ever discarded a model's own decided matches and never added new
23-
# ones. Per-pair scores are averaged across both directions before ranking
24-
# (mirrors the notebooks' avg_across_directions()), and ties are broken by a
25-
# random draw (mirrors get_ordered_matches()'s random_tiebreaker column) --
26-
# not a full directional-filter/conflict-resolution pass, which the notebooks
27-
# also run before ranking (a one-to-one matching constraint) and this script
28-
# does not reproduce; see _rank_based_fixed_n_matches()'s docstring.
14+
# REFERENCE_MODEL's own conflict-resolved match count for that same pair (an
15+
# unordered-unit-pair count, not identity -- see _build_reference_n_lookup()).
16+
# This mirrors fast_testing.py's get_matches_1model(fixed_n=...) closely: the
17+
# ranking operates over the *full* across-session candidate pool for that
18+
# pair, not just the pipeline's own already-decided matches, so a model can
19+
# both lose matches it had decided on (if they rank low) and gain ones it
20+
# hadn't (if they rank in the top N despite being below that model's own
21+
# decision threshold) -- unlike an earlier, more conservative version of this
22+
# script that only ever discarded a model's own decided matches and never
23+
# added new ones. Per-pair scores are averaged across both directions before
24+
# ranking (mirrors the notebooks' avg_across_directions()), one-to-many
25+
# conflicts are resolved on that averaged score before ranking (mirrors
26+
# remove_conflicts2(), and runs in the same order fast_testing uses: resolve
27+
# conflicts, then take the top N), and ties are broken by a random draw
28+
# (mirrors get_ordered_matches()'s random_tiebreaker column) -- see
29+
# _rank_based_fixed_n_matches()'s docstring for what's still not reproduced
30+
# (directional_filter's require-both-directions-above-threshold check).
2931
#
3032
# "UM Probabilities" is only a real, continuously-rankable score for
3133
# DeepUnitMatch/UMPy-family models -- EMD/DANT/DANT_no_functional save
@@ -100,8 +102,16 @@
100102
def _build_reference_n_lookup(session_date_lookup, base_output=BASE_OUTPUT, reference_model=REFERENCE_MODEL):
101103
"""
102104
{dataset: {(RecSes1, RecSes2) with RecSes1 < RecSes2: N}} -- N = how many
103-
unordered unit pairs reference_model's own pipeline decision ("Matches")
104-
called a match for that session pair, in that dataset.
105+
unordered unit pairs reference_model committed to as a match for that
106+
session pair, in that dataset, AFTER resolving one-to-many conflicts
107+
(dvd._resolve_match_conflicts(), ranked by "UM Probabilities" same as
108+
everywhere else) -- not the raw pipeline "Matches" column's count, which
109+
has no uniqueness constraint at write time (see
110+
dvd._resolve_match_conflicts()'s docstring) and so can overcount relative
111+
to what reference_model actually settled on for a unit. This mirrors
112+
fast_testing.py's own reference N: it comes from a prior
113+
get_matches_1model(fixed_n=False) run, whose remove_conflicts2() always
114+
resolves conflicts before N = len(match_indices) is recorded.
105115
106116
Datasets/pairs missing here get treated as N=0 by
107117
_rank_based_fixed_n_matches() (every other model's matches for that pair
@@ -142,11 +152,14 @@ def _build_reference_n_lookup(session_date_lookup, base_output=BASE_OUTPUT, refe
142152
if not os.path.isfile(match_table_path):
143153
continue
144154

155+
header = pd.read_csv(match_table_path, nrows=0).columns
145156
usecols = ["ID1", "ID2", "RecSes 1", "RecSes 2", "Matches"]
146157
dtype_map = {"ID1": "int32", "ID2": "int32", "RecSes 1": "int32", "RecSes 2": "int32", "Matches": "int8"}
158+
if dvd.MATCH_CONFLICT_RANK_COL in header:
159+
usecols.append(dvd.MATCH_CONFLICT_RANK_COL)
160+
dtype_map[dvd.MATCH_CONFLICT_RANK_COL] = "float32"
147161
if variant is not None:
148162
_, (uid_col_a, uid_col_b) = variant
149-
header = pd.read_csv(match_table_path, nrows=0).columns
150163
if uid_col_a not in header or uid_col_b not in header:
151164
print(f" {match_table_path} missing '{uid_col_a}'/'{uid_col_b}', skipping reference lookup for {dataset}.")
152165
continue
@@ -158,6 +171,8 @@ def _build_reference_n_lookup(session_date_lookup, base_output=BASE_OUTPUT, refe
158171
_, (uid_col_a, uid_col_b) = variant
159172
df["Matches"] = (df[uid_col_a] == df[uid_col_b]).astype(np.int8)
160173

174+
df = df.assign(Matches=dvd._resolve_match_conflicts(df))
175+
161176
pos = df[(df["RecSes 1"] < df["RecSes 2"]) & (df["Matches"] == 1)]
162177
dup_flagged = dvd._build_within_session_duplicate_lookup(base_output, {dataset}).get(dataset, set())
163178
pos, _ = dvd._drop_within_session_duplicate_pairs(pos, dup_flagged)
@@ -190,13 +205,20 @@ def _rank_based_fixed_n_matches(df, ref_counts_for_dataset, rng, rank_col="UM Pr
190205
(mirrors get_ordered_matches()'s random_tiebreaker column).
191206
192207
Not reproduced here: the notebooks additionally run directional_filter
193-
(require both directions individually above threshold) and
194-
remove_conflicts2 (a one-to-one matching constraint: at most one partner
195-
per unit) before ranking. Averaging across directions already softly
196-
penalises one-way-only candidates rather than excluding them outright,
197-
and conflicts aren't resolved -- a unit can appear in more than one of
198-
the selected top-N pairs. Both are simplifications relative to the
199-
notebooks' own fixed_n comparison.
208+
(require both directions individually above threshold) before ranking.
209+
Averaging across directions already softly penalises one-way-only
210+
candidates rather than excluding them outright -- a simplification
211+
relative to the notebooks' own fixed_n comparison. One-to-many conflicts
212+
(remove_conflicts2's one-partner-per-unit constraint) ARE resolved here,
213+
on the direction-averaged score, before top-N ranking -- same order
214+
fast_testing.get_matches_1model(fixed_n=...) uses (remove_conflicts2()
215+
runs, THEN .head(fixed_n) picks winners from what's left), rather than
216+
collect_binned_pairs()'s own post-hoc resolve_match_conflicts pass, which
217+
would otherwise run after N is already fixed (on raw per-direction
218+
"UM Probabilities" values, a different score source) and could shrink the
219+
selected set below N. main() passes resolve_match_conflicts=False to
220+
collect_binned_pairs() for this reason -- conflict resolution happens
221+
exactly once, here.
200222
"""
201223
r1 = df["RecSes 1"].to_numpy()
202224
r2 = df["RecSes 2"].to_numpy()
@@ -220,11 +242,32 @@ def _rank_based_fixed_n_matches(df, ref_counts_for_dataset, rng, rank_col="UM Pr
220242
canon["canon_id"] = canon.groupby(["sess_lo", "unit_lo", "sess_hi", "unit_hi"], sort=False).ngroup()
221243

222244
pairs = canon.groupby("canon_id", sort=False).agg(
223-
sess_lo=("sess_lo", "first"), sess_hi=("sess_hi", "first"), avg_score=("score", "mean"),
245+
sess_lo=("sess_lo", "first"), sess_hi=("sess_hi", "first"),
246+
unit_lo=("unit_lo", "first"), unit_hi=("unit_hi", "first"),
247+
avg_score=("score", "mean"),
224248
)
225249
# NaN scores (shouldn't normally occur -- UM Probabilities is computed for
226-
# every candidate pair -- but handled defensively) never win a ranking.
227-
avg_score = np.nan_to_num(pairs["avg_score"].to_numpy(), nan=-np.inf)
250+
# every candidate pair -- but handled defensively) never win a ranking or
251+
# a conflict.
252+
pairs["avg_score"] = pairs["avg_score"].fillna(-np.inf)
253+
254+
# Resolve one-to-many conflicts per session pair before ranking (see
255+
# docstring above): a candidate survives only if its avg_score is
256+
# simultaneously maximal among every other candidate sharing its "lo"
257+
# endpoint unit and every other candidate sharing its "hi" endpoint unit,
258+
# within the same session pair -- same both-endpoints-maximal rule as
259+
# dvd._resolve_match_conflicts()/fast_testing.remove_conflicts2(), just
260+
# applied to canonical (direction-averaged) candidates instead of raw
261+
# rows. Losing candidates are dropped from ranking entirely (rather than
262+
# zeroed-but-still-eligible, as remove_conflicts2 does) so a conflict can
263+
# never consume one of the N slots -- a minor, deliberate simplification
264+
# that only differs from fast_testing in the rare case where a session
265+
# pair has fewer than N conflict-free candidates.
266+
max_for_lo = pairs.groupby(["sess_lo", "sess_hi", "unit_lo"])["avg_score"].transform("max")
267+
max_for_hi = pairs.groupby(["sess_lo", "sess_hi", "unit_hi"])["avg_score"].transform("max")
268+
pairs = pairs[(pairs["avg_score"] == max_for_lo) & (pairs["avg_score"] == max_for_hi)]
269+
270+
avg_score = pairs["avg_score"].to_numpy()
228271
tiebreak = rng.random(len(pairs))
229272
rows_by_canon = canon.groupby("canon_id", sort=False)["row"].apply(np.asarray)
230273

@@ -278,6 +321,14 @@ def matches_transform(dataset, df):
278321
models_to_include=RANK_ELIGIBLE_MODELS,
279322
matches_transform=matches_transform,
280323
extra_usecols={"ID2": "int32", "UM Probabilities": "float32"},
324+
# _rank_based_fixed_n_matches() already resolves one-to-many conflicts
325+
# itself, before picking the top N (see its docstring) -- running
326+
# collect_binned_pairs()'s own post-hoc pass here too would re-decide
327+
# conflicts from raw per-direction "UM Probabilities" values (a
328+
# different score source than the direction-averaged one used for
329+
# ranking) after N is already fixed, and could shrink a model's
330+
# selected set below N.
331+
resolve_match_conflicts=False,
281332
)
282333

283334
auc_df = dvd.summarise_auc(score_bins)

0 commit comments

Comments
 (0)