refitgap 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- refitgap/API.txt +150 -0
- refitgap/__init__.py +29 -0
- refitgap/__main__.py +6 -0
- refitgap/analysis.py +399 -0
- refitgap/cli.py +173 -0
- refitgap/crossfit.py +275 -0
- refitgap/example.py +96 -0
- refitgap/fitters.py +114 -0
- refitgap/identification.py +101 -0
- refitgap/inference.py +319 -0
- refitgap/plots.py +214 -0
- refitgap/records.py +38 -0
- refitgap/report.py +136 -0
- refitgap/runs.py +53 -0
- refitgap/tiny.py +68 -0
- refitgap/twoscorer.py +504 -0
- refitgap-0.1.0.dist-info/METADATA +128 -0
- refitgap-0.1.0.dist-info/RECORD +22 -0
- refitgap-0.1.0.dist-info/WHEEL +5 -0
- refitgap-0.1.0.dist-info/entry_points.txt +2 -0
- refitgap-0.1.0.dist-info/licenses/LICENSE +21 -0
- refitgap-0.1.0.dist-info/top_level.txt +1 -0
refitgap/twoscorer.py
ADDED
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
"""Two scorers on the same records at one carried rule, and the paired method contrast.
|
|
2
|
+
|
|
3
|
+
compare() takes a reference and a final scorer on the same labelled records and one carried
|
|
4
|
+
threshold vector; method_contrast() gives method a's lead over b, carried and refitted, paired
|
|
5
|
+
by seed. Every level comes from evaluate(), every component from decompose(), every interval
|
|
6
|
+
from inference.paired_contrast()."""
|
|
7
|
+
import warnings
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
from . import inference
|
|
12
|
+
from .crossfit import IDENTITY_TOL, SPLITS, SPLIT_BASE, decompose, evaluate
|
|
13
|
+
from .fitters import fit_exact, macro_f1
|
|
14
|
+
from .records import align_to
|
|
15
|
+
|
|
16
|
+
TIE_CONVENTION = ("a record is predicted positive when score >= threshold; fitted cutpoints are score values, so "
|
|
17
|
+
"tied records are predicted together; when two cutpoints give the same F1 the own-period fitter "
|
|
18
|
+
"chooses the higher; the shared fitter maximises a float sum over periods, so two cutpoints whose "
|
|
19
|
+
"objectives are equal in exact arithmetic may differ in the last bit and either can be chosen")
|
|
20
|
+
ABSENT_LABEL_CONVENTION = ("a label with no positive in a period has F1 0 there whatever is predicted "
|
|
21
|
+
"(zero_division = 0) and still counts in the macro mean; its fitted cutpoint lies above "
|
|
22
|
+
"every score")
|
|
23
|
+
SPLIT_RULE = ("partition b of period id j, seed s: b-th permutation of numpy default_rng(split_base + 100*s + j); "
|
|
24
|
+
"half 0 is the first n_j // 2 records; every rule is fitted on one half and scored on the other, "
|
|
25
|
+
"in both directions")
|
|
26
|
+
SIGNS = dict(deficit="U_ref - U_final (positive: the final scorer is worse at the carried rule)",
|
|
27
|
+
attainable_gap="V_ref - V_final (positive: the final scorer is worse after its own refit)",
|
|
28
|
+
mismatch_change="R_final - R_ref, R_x = V_x - U_x (positive: the carried rule fits the final "
|
|
29
|
+
"scorer worse than it fits the reference)",
|
|
30
|
+
identity="deficit = attainable_gap + mismatch_change")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# ------------------------------------------------------------------ input handling ---
|
|
34
|
+
def _periods(x, what):
|
|
35
|
+
"""One (n, K) array, or a list/tuple of them (one per period). Returns (list, single).
|
|
36
|
+
A list or tuple always means one matrix per period, so a single matrix written as a
|
|
37
|
+
nested Python list (rows of numbers) is refused rather than read as one period per row."""
|
|
38
|
+
if isinstance(x, (list, tuple)):
|
|
39
|
+
if len(x) == 0:
|
|
40
|
+
raise ValueError(f"{what}: empty list of periods")
|
|
41
|
+
arrs = [np.asarray(a) for a in x]
|
|
42
|
+
if any(a.ndim == 0 for a in arrs):
|
|
43
|
+
raise ValueError(f"{what}: a list of numbers, not a list of per-period matrices; pass one period as a "
|
|
44
|
+
"numpy array, or several as a list with one (n_j, K) matrix per period")
|
|
45
|
+
if all(a.ndim == 1 for a in arrs) and len({a.shape for a in arrs}) == 1:
|
|
46
|
+
raise ValueError(f"{what}: a nested list that reads as one {len(arrs)} x {arrs[0].shape[0]} matrix, "
|
|
47
|
+
"while a list means one matrix per period; pass np.asarray(...) for one period, or a "
|
|
48
|
+
"list with one 2-d (n_j, K) matrix per period (an (n_j, 1) matrix for one label)")
|
|
49
|
+
return arrs, False
|
|
50
|
+
return [np.asarray(x)], True
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _as_matrix(a, what):
|
|
54
|
+
a = np.asarray(a)
|
|
55
|
+
if a.ndim == 1:
|
|
56
|
+
a = a[:, None]
|
|
57
|
+
if a.ndim != 2:
|
|
58
|
+
raise ValueError(f"{what}: need a (records, labels) matrix, got shape {a.shape}")
|
|
59
|
+
return a
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _ids_per_period(ids, N, single, what):
|
|
63
|
+
if single:
|
|
64
|
+
return [list(ids)]
|
|
65
|
+
if not isinstance(ids, (list, tuple)) or len(ids) != N:
|
|
66
|
+
raise ValueError(f"{what}: one id sequence per period ({N} periods)")
|
|
67
|
+
return [list(x) for x in ids]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _check_ids(ref_ids, ids, what, period):
|
|
71
|
+
if len(set(ids)) != len(ids):
|
|
72
|
+
dup = next(i for i in ids if ids.count(i) > 1)
|
|
73
|
+
raise ValueError(f"period {period}: duplicate record id in {what} (e.g. {dup!r})")
|
|
74
|
+
a, b = set(ref_ids), set(ids)
|
|
75
|
+
if a != b:
|
|
76
|
+
raise ValueError(f"period {period}: the record ids of {what} and of the labels differ "
|
|
77
|
+
f"({len(b - a)} only in {what}, {len(a - b)} only in the labels); "
|
|
78
|
+
"compare() needs the same records for both scorers")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _column_order(names_lab, names, what):
|
|
82
|
+
names_lab, names = list(names_lab), list(names)
|
|
83
|
+
for nm, w in ((names_lab, "labels"), (names, what)):
|
|
84
|
+
if len(set(nm)) != len(nm):
|
|
85
|
+
raise ValueError(f"label_ids of {w} contain duplicates")
|
|
86
|
+
if set(names_lab) != set(names):
|
|
87
|
+
raise ValueError(f"label_ids of {what} and of the labels are different label sets")
|
|
88
|
+
pos = {n: i for i, n in enumerate(names)}
|
|
89
|
+
return np.array([pos[n] for n in names_lab], dtype=np.int64)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _prepare(scores_ref, scores_final, labels, rule, record_ids, label_ids, calibration_mask, labels_final,
|
|
93
|
+
period_ids, score_scale=None):
|
|
94
|
+
P_ref, single = _periods(scores_ref, "scores_ref")
|
|
95
|
+
P_fin, s2 = _periods(scores_final, "scores_final")
|
|
96
|
+
Y, s3 = _periods(labels, "labels")
|
|
97
|
+
if not (single == s2 == s3) or not (len(P_ref) == len(P_fin) == len(Y)):
|
|
98
|
+
raise ValueError("scores_ref, scores_final and labels must all be one matrix, or all lists with one "
|
|
99
|
+
"matrix per period")
|
|
100
|
+
N = len(Y)
|
|
101
|
+
Yf = None if labels_final is None else _periods(labels_final, "labels_final")[0]
|
|
102
|
+
if Yf is not None and len(Yf) != N:
|
|
103
|
+
raise ValueError("labels_final: one matrix per period")
|
|
104
|
+
period_ids = list(range(N)) if period_ids is None else [int(p) for p in period_ids]
|
|
105
|
+
if len(period_ids) != N:
|
|
106
|
+
raise ValueError(f"period_ids: one id per period ({N})")
|
|
107
|
+
P_ref = [_as_matrix(a, "scores_ref") for a in P_ref]
|
|
108
|
+
P_fin = [_as_matrix(a, "scores_final") for a in P_fin]
|
|
109
|
+
Y = [_as_matrix(a, "labels") for a in Y]
|
|
110
|
+
Yf = None if Yf is None else [_as_matrix(a, "labels_final") for a in Yf]
|
|
111
|
+
|
|
112
|
+
# records: align the scorers (and labels_final, which travels with the final scorer) to the labels' ids
|
|
113
|
+
if record_ids is not None:
|
|
114
|
+
rid = record_ids if isinstance(record_ids, dict) else dict(labels=record_ids)
|
|
115
|
+
bad = set(rid) - {"ref", "final", "labels"}
|
|
116
|
+
if bad or "labels" not in rid:
|
|
117
|
+
raise ValueError("record_ids: one id sequence (shared by every input) or a dict with key 'labels' "
|
|
118
|
+
"and optional keys 'ref' and 'final'")
|
|
119
|
+
ids_lab = _ids_per_period(rid["labels"], N, single, "record_ids['labels']")
|
|
120
|
+
ids_ref = _ids_per_period(rid.get("ref", rid["labels"]), N, single, "record_ids['ref']")
|
|
121
|
+
ids_fin = _ids_per_period(rid.get("final", rid["labels"]), N, single, "record_ids['final']")
|
|
122
|
+
for c in range(N):
|
|
123
|
+
j = period_ids[c]
|
|
124
|
+
for ids, a, w in ((ids_lab[c], Y[c], "labels"), (ids_ref[c], P_ref[c], "scores_ref"),
|
|
125
|
+
(ids_fin[c], P_fin[c], "scores_final")):
|
|
126
|
+
if len(ids) != a.shape[0]:
|
|
127
|
+
raise ValueError(f"period {j}: {w} has {a.shape[0]} rows for {len(ids)} record ids")
|
|
128
|
+
_check_ids(ids_lab[c], ids_lab[c], "the labels", j)
|
|
129
|
+
_check_ids(ids_lab[c], ids_ref[c], "scores_ref", j)
|
|
130
|
+
_check_ids(ids_lab[c], ids_fin[c], "scores_final", j)
|
|
131
|
+
P_ref[c] = align_to(ids_lab[c], ids_ref[c], P_ref[c])
|
|
132
|
+
if Yf is not None:
|
|
133
|
+
P_fin[c], Yf[c] = align_to(ids_lab[c], ids_fin[c], P_fin[c], Yf[c])
|
|
134
|
+
else:
|
|
135
|
+
P_fin[c] = align_to(ids_lab[c], ids_fin[c], P_fin[c])
|
|
136
|
+
|
|
137
|
+
# labels (columns): reorder the scorers' columns, and a rule given by name, to the labels' column order
|
|
138
|
+
names = None
|
|
139
|
+
if label_ids is not None:
|
|
140
|
+
lid = label_ids if isinstance(label_ids, dict) else dict(labels=label_ids)
|
|
141
|
+
bad = set(lid) - {"ref", "final", "labels"}
|
|
142
|
+
if bad or "labels" not in lid:
|
|
143
|
+
raise ValueError("label_ids: one name sequence (shared by every input) or a dict with key 'labels' "
|
|
144
|
+
"and optional keys 'ref' and 'final'")
|
|
145
|
+
names = [x for x in lid["labels"]]
|
|
146
|
+
o_ref = _column_order(names, lid.get("ref", names), "scores_ref")
|
|
147
|
+
o_fin = _column_order(names, lid.get("final", names), "scores_final")
|
|
148
|
+
for c in range(N):
|
|
149
|
+
for a, w in ((Y[c], "labels"), (P_ref[c], "scores_ref"), (P_fin[c], "scores_final")):
|
|
150
|
+
if a.shape[1] != len(names):
|
|
151
|
+
raise ValueError(f"period {period_ids[c]}: {w} has {a.shape[1]} columns for {len(names)} label ids")
|
|
152
|
+
P_ref[c], P_fin[c] = P_ref[c][:, o_ref], P_fin[c][:, o_fin]
|
|
153
|
+
if Yf is not None:
|
|
154
|
+
Yf[c] = Yf[c][:, o_fin]
|
|
155
|
+
if isinstance(rule, dict):
|
|
156
|
+
if names is None:
|
|
157
|
+
raise ValueError("a rule given as {label: threshold} needs label_ids")
|
|
158
|
+
if set(rule) != set(names):
|
|
159
|
+
raise ValueError("the rule's labels and the labels' label_ids are different label sets")
|
|
160
|
+
rule = np.array([rule[n] for n in names], np.float64)
|
|
161
|
+
rule = np.asarray(rule, np.float64).reshape(-1)
|
|
162
|
+
|
|
163
|
+
# shapes, labels, scores
|
|
164
|
+
K = Y[0].shape[1]
|
|
165
|
+
for c in range(N):
|
|
166
|
+
j = period_ids[c]
|
|
167
|
+
for a, w in ((P_ref[c], "scores_ref"), (P_fin[c], "scores_final")):
|
|
168
|
+
if a.shape != Y[c].shape:
|
|
169
|
+
raise ValueError(f"period {j}: {w} has shape {a.shape}, the labels {Y[c].shape}")
|
|
170
|
+
if Y[c].shape[1] != K:
|
|
171
|
+
raise ValueError(f"period {j}: {Y[c].shape[1]} labels, period {period_ids[0]} has {K}")
|
|
172
|
+
if not np.isin(Y[c], (0, 1)).all():
|
|
173
|
+
raise ValueError(f"period {j}: labels must be 0/1 (remove unlabelled records with drop_unlabelled)")
|
|
174
|
+
if Yf is not None:
|
|
175
|
+
if Yf[c].shape != Y[c].shape:
|
|
176
|
+
raise ValueError(f"period {j}: labels_final has shape {Yf[c].shape}, the labels {Y[c].shape}")
|
|
177
|
+
ndiff = int((np.asarray(Yf[c]) != np.asarray(Y[c])).sum())
|
|
178
|
+
if ndiff:
|
|
179
|
+
raise ValueError(f"period {j}: the two label matrices differ in {ndiff} cells after alignment; "
|
|
180
|
+
"both scorers must be evaluated against the same labels")
|
|
181
|
+
if rule.shape != (K,):
|
|
182
|
+
raise ValueError(f"rule: one threshold per label ({K}), got {rule.shape[0]}")
|
|
183
|
+
if not np.all(np.isfinite(rule)):
|
|
184
|
+
raise ValueError("rule: non-finite threshold")
|
|
185
|
+
|
|
186
|
+
# calibration mask: which records' labels the evaluation may use (in the labels' record order)
|
|
187
|
+
n_all = [int(y.shape[0]) for y in Y]
|
|
188
|
+
if calibration_mask is not None:
|
|
189
|
+
masks = [np.asarray(m) for m in (calibration_mask if not single else [calibration_mask])]
|
|
190
|
+
if len(masks) != N:
|
|
191
|
+
raise ValueError("calibration_mask: one boolean vector per period")
|
|
192
|
+
for c in range(N):
|
|
193
|
+
m = masks[c]
|
|
194
|
+
if m.dtype != bool or m.shape != (n_all[c],):
|
|
195
|
+
raise ValueError(f"period {period_ids[c]}: calibration_mask must be a boolean vector of "
|
|
196
|
+
f"length {n_all[c]} (the labels' records, in the labels' order)")
|
|
197
|
+
P_ref[c], P_fin[c], Y[c] = P_ref[c][m], P_fin[c][m], Y[c][m]
|
|
198
|
+
|
|
199
|
+
scale = "probability"
|
|
200
|
+
for c in range(N):
|
|
201
|
+
j = period_ids[c]
|
|
202
|
+
if Y[c].shape[0] < 2:
|
|
203
|
+
raise ValueError(f"period {j}: needs at least 2 labelled records to split in half")
|
|
204
|
+
for a, w in ((P_ref[c], "scores_ref"), (P_fin[c], "scores_final")):
|
|
205
|
+
if not np.all(np.isfinite(a)):
|
|
206
|
+
raise ValueError(f"period {j}: {w} has non-finite scores")
|
|
207
|
+
outside = [w for c in range(N) for a, w in ((P_ref[c], "scores_ref"), (P_fin[c], "scores_final"))
|
|
208
|
+
if a.size and (a.min() < 0 or a.max() > 1)]
|
|
209
|
+
if score_scale not in (None, "other"):
|
|
210
|
+
raise ValueError("score_scale: None (probability scale expected) or 'other'")
|
|
211
|
+
rule_out = bool(np.any((rule < 0) | (rule > 1)))
|
|
212
|
+
if outside or rule_out or score_scale == "other":
|
|
213
|
+
if score_scale is None and not rule_out:
|
|
214
|
+
raise ValueError(f"{sorted(set(outside))[0]} has scores outside [0, 1] while the rule lies in [0, 1]: "
|
|
215
|
+
"the scores and the carried rule look like different scales (for example logits "
|
|
216
|
+
"against a probability threshold). Map the scores onto the rule's scale first, "
|
|
217
|
+
"or pass score_scale='other' if both are on one other scale.")
|
|
218
|
+
if score_scale is None:
|
|
219
|
+
what = ("the rule lies outside [0, 1] while every score lies in [0, 1]: the scores and the carried rule "
|
|
220
|
+
"look like different scales (for example a logit threshold against probabilities)" if not outside
|
|
221
|
+
else "the scores and the rule lie outside [0, 1], so they are not on the probability scale")
|
|
222
|
+
raise ValueError(f"{what}. Map them onto one scale first, or pass score_scale='other' if both are on one "
|
|
223
|
+
"other scale.")
|
|
224
|
+
scale = "other"
|
|
225
|
+
warnings.warn("scores or rule not on the probability scale; the levels are valid only if the scores and "
|
|
226
|
+
"the carried rule are on the same scale", UserWarning, stacklevel=3)
|
|
227
|
+
return P_ref, P_fin, [y.astype(int) for y in Y], rule, names, period_ids, single, n_all, scale
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
# ------------------------------------------------------------------ compare ---
|
|
231
|
+
def _components(d):
|
|
232
|
+
out = dict(deficit=d["D"], attainable_gap=d["A"], mismatch_change=d["R"], mismatch_ref=d["R_ref"],
|
|
233
|
+
mismatch_final=d["R_final"])
|
|
234
|
+
if d.get("shared_terms") == "reported":
|
|
235
|
+
out.update(attainable_gap_shared=d["A_shared"], shared_cost_change=d["dC"], shared_mismatch_change=d["dM"],
|
|
236
|
+
shared_cost_ref=d["C_ref"], shared_cost_final=d["C_final"], shared_mismatch_ref=d["M_ref"],
|
|
237
|
+
shared_mismatch_final=d["M_final"])
|
|
238
|
+
return out
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _block(d, sr, sf, rule_name):
|
|
242
|
+
out = dict(carried=dict(ref=d["U_ref"], final=d["U_final"], convention=d["U"],
|
|
243
|
+
ref_half=sr["rules"][rule_name]["U_half"], final_half=sf["rules"][rule_name]["U_half"],
|
|
244
|
+
ref_full=sr["rules"][rule_name]["U_full"], final_full=sf["rules"][rule_name]["U_full"]),
|
|
245
|
+
refitted=dict(ref=d["V_ref"], final=d["V_final"]),
|
|
246
|
+
components=_components(d))
|
|
247
|
+
if sr["V_shared"] is not None and sf["V_shared"] is not None:
|
|
248
|
+
out["refitted_shared"] = dict(ref=sr["V_shared"], final=sf["V_shared"])
|
|
249
|
+
return out
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def compare(scores_ref, scores_final, labels, rule, *, record_ids=None, label_ids=None, seed=None, splits=SPLITS,
|
|
253
|
+
calibration_mask=None, labels_final=None, period_ids=None, halves=False, rule_name="carried",
|
|
254
|
+
score_scale=None, fixed_split=None, fixed_split_by=None):
|
|
255
|
+
"""Two (n, K) score matrices, (n, K) 0/1 labels and a (K,) carried rule (lists for several
|
|
256
|
+
periods) -> dict(carried, refitted, components, per_period, metadata, decomposition,
|
|
257
|
+
evaluations): both scorers' carried and refitted macro-F1, and deficit = attainable gap +
|
|
258
|
+
change in mismatch.
|
|
259
|
+
|
|
260
|
+
record_ids / label_ids: one sequence (inputs already in that order) or a dict of each
|
|
261
|
+
input's own ids, which are aligned to the labels; duplicates or mismatched sets raise.
|
|
262
|
+
seed keys the partitions (required unless fixed_split). splits: partitions per period.
|
|
263
|
+
calibration_mask: records whose labels may be used. labels_final: the labels exported with
|
|
264
|
+
the final scorer, checked cell by cell. halves: carried level on held-out halves.
|
|
265
|
+
score_scale=None wants scores and rule in [0, 1]; "other" declares another common scale.
|
|
266
|
+
fixed_split={"calibration": c, "evaluation": e} (masks, positions or ids; fixed_split_by
|
|
267
|
+
says which) swaps the cross-fitted estimator for one fixed split. Labels with no positive
|
|
268
|
+
take F1 0 and stay in the macro mean."""
|
|
269
|
+
from . import __version__
|
|
270
|
+
P_ref, P_fin, Y, rule, names, period_ids, single, n_all, scale = _prepare(
|
|
271
|
+
scores_ref, scores_final, labels, rule, record_ids, label_ids, calibration_mask, labels_final, period_ids,
|
|
272
|
+
score_scale)
|
|
273
|
+
if fixed_split is not None:
|
|
274
|
+
return _compare_fixed(P_ref, P_fin, Y, rule, names, period_ids, single, n_all, scale, record_ids,
|
|
275
|
+
calibration_mask, fixed_split, rule_name, __version__, fixed_split_by)
|
|
276
|
+
if fixed_split_by is not None:
|
|
277
|
+
raise ValueError("fixed_split_by applies only with fixed_split")
|
|
278
|
+
if seed is None:
|
|
279
|
+
raise ValueError("seed is required for the cross-fitted estimator (it keys the partitions)")
|
|
280
|
+
rules = {rule_name: rule}
|
|
281
|
+
ev_ref = evaluate(P_ref, Y, seed, period_ids, rules=rules, splits=splits)
|
|
282
|
+
ev_fin = evaluate(P_fin, Y, seed, period_ids, rules=rules, splits=splits)
|
|
283
|
+
d = decompose(ev_ref, ev_fin, rule_name, halves=halves)
|
|
284
|
+
out = _block(d, ev_ref.summary(), ev_fin.summary(), rule_name)
|
|
285
|
+
out["per_period"] = {}
|
|
286
|
+
for j in period_ids:
|
|
287
|
+
dj = decompose(ev_ref, ev_fin, rule_name, periods=[j], halves=halves)
|
|
288
|
+
out["per_period"][j] = _block(dj, ev_ref.summary([j]), ev_fin.summary([j]), rule_name)
|
|
289
|
+
K = Y[0].shape[1]
|
|
290
|
+
lab = (lambda k: names[k]) if names is not None else (lambda k: k)
|
|
291
|
+
absent = {j: [lab(k) for k in range(K) if Y[c][:, k].sum() == 0] for c, j in enumerate(period_ids)}
|
|
292
|
+
budget = {j: dict(records=int(len(Y[c])), records_before_mask=n_all[c],
|
|
293
|
+
fit_records_per_direction=[int(len(Y[c]) // 2), int(len(Y[c]) - len(Y[c]) // 2)],
|
|
294
|
+
positives_per_label=[int(v) for v in Y[c].sum(0)])
|
|
295
|
+
for c, j in enumerate(period_ids)}
|
|
296
|
+
out["metadata"] = dict(
|
|
297
|
+
rule_name=rule_name, rule=rule.tolist(), label_ids=names, n_labels=K, period_ids=period_ids,
|
|
298
|
+
single_period=single, seed=int(seed), splits=int(splits), split_base=SPLIT_BASE, split_rule=SPLIT_RULE,
|
|
299
|
+
carried_level=d["U"], label_budget=budget,
|
|
300
|
+
label_budget_note=("labelled records each refit is fitted on, per period and direction (half 0 then half 1); "
|
|
301
|
+
"the shared rule pools the fitting halves of all periods"),
|
|
302
|
+
calibration_mask_used=calibration_mask is not None, ties=TIE_CONVENTION,
|
|
303
|
+
absent_labels=ABSENT_LABEL_CONVENTION, labels_without_positive={j: v for j, v in absent.items() if v},
|
|
304
|
+
periods_without_positive=[j for c, j in enumerate(period_ids) if Y[c].sum() == 0],
|
|
305
|
+
score_scale=scale, signs=SIGNS, shared_terms=d["shared_terms"],
|
|
306
|
+
estimator="crossfit.evaluate on each scorer, crossfit.decompose on the pair (split-half cross-fitting)",
|
|
307
|
+
package_version=__version__)
|
|
308
|
+
out["decomposition"] = d
|
|
309
|
+
out["evaluations"] = dict(ref=ev_ref, final=ev_fin)
|
|
310
|
+
return out
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
# ------------------------------------------------------------------ fixed calibration split ---
|
|
314
|
+
FIXED_SPLIT_LABEL = "fixed calibration split, not the paper's cross-fitted estimator"
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
FIXED_SPLIT_BY = (None, "ids", "positions")
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _is_int(x):
|
|
321
|
+
return isinstance(x, (int, np.integer)) and not isinstance(x, (bool, np.bool_))
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _positions(sel, n, ids, what, j, by=None):
|
|
325
|
+
"""One fixed_split set -> row positions. Masks are masks; otherwise `by` ("positions" or
|
|
326
|
+
"ids", or a guess when None). Values valid both ways that select different rows raise."""
|
|
327
|
+
sel = np.asarray(sel)
|
|
328
|
+
if sel.dtype == bool:
|
|
329
|
+
if sel.shape != (n,):
|
|
330
|
+
raise ValueError(f"period {j}: fixed_split['{what}'] mask must have length {n}")
|
|
331
|
+
return np.flatnonzero(sel)
|
|
332
|
+
if by is None:
|
|
333
|
+
if ids is None or (np.issubdtype(sel.dtype, np.integer) and not all(_is_int(r) for r in ids)):
|
|
334
|
+
by = "positions"
|
|
335
|
+
elif not np.issubdtype(sel.dtype, np.integer):
|
|
336
|
+
by = "ids"
|
|
337
|
+
else:
|
|
338
|
+
pos = {r: i for i, r in enumerate(ids)}
|
|
339
|
+
vals = sel.reshape(-1).tolist()
|
|
340
|
+
if all(v in pos for v in vals) and all(0 <= v < n for v in vals) and any(pos[v] != v for v in vals):
|
|
341
|
+
raise ValueError(f"period {j}: fixed_split['{what}'] holds integers that are valid record ids and "
|
|
342
|
+
"valid row positions, and the two readings select different records; pass "
|
|
343
|
+
"fixed_split_by='ids' or fixed_split_by='positions'")
|
|
344
|
+
by = "ids"
|
|
345
|
+
if by == "ids" and ids is None:
|
|
346
|
+
raise ValueError("fixed_split_by='ids' needs record_ids")
|
|
347
|
+
if by == "ids":
|
|
348
|
+
pos = {r: i for i, r in enumerate(ids)}
|
|
349
|
+
miss = [r for r in sel.tolist() if r not in pos]
|
|
350
|
+
if miss:
|
|
351
|
+
raise ValueError(f"period {j}: fixed_split['{what}'] has unknown record id {miss[0]!r}")
|
|
352
|
+
out = np.array([pos[r] for r in sel.tolist()], dtype=np.int64)
|
|
353
|
+
else:
|
|
354
|
+
out = sel.astype(np.int64).reshape(-1)
|
|
355
|
+
if out.size and (out.min() < 0 or out.max() >= n):
|
|
356
|
+
raise ValueError(f"period {j}: fixed_split['{what}'] positions outside 0..{n - 1}")
|
|
357
|
+
if len(set(out.tolist())) != len(out):
|
|
358
|
+
raise ValueError(f"period {j}: fixed_split['{what}'] lists a record twice")
|
|
359
|
+
return out
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _compare_fixed(P_ref, P_fin, Y, rule, names, period_ids, single, n_all, scale, record_ids, calibration_mask,
|
|
363
|
+
fixed_split, rule_name, version, fixed_split_by=None):
|
|
364
|
+
if fixed_split_by not in FIXED_SPLIT_BY:
|
|
365
|
+
raise ValueError("fixed_split_by: None, 'ids' or 'positions'")
|
|
366
|
+
if calibration_mask is not None:
|
|
367
|
+
raise ValueError("pass either calibration_mask or fixed_split, not both")
|
|
368
|
+
if not isinstance(fixed_split, dict) or set(fixed_split) != {"calibration", "evaluation"}:
|
|
369
|
+
raise ValueError("fixed_split: {'calibration': ..., 'evaluation': ...}")
|
|
370
|
+
N = len(Y)
|
|
371
|
+
cal, ev = fixed_split["calibration"], fixed_split["evaluation"]
|
|
372
|
+
cal, ev = ([cal], [ev]) if single else (list(cal), list(ev))
|
|
373
|
+
if len(cal) != N or len(ev) != N:
|
|
374
|
+
raise ValueError(f"fixed_split: one calibration and one evaluation set per period ({N})")
|
|
375
|
+
ids = None
|
|
376
|
+
if record_ids is not None:
|
|
377
|
+
lab = record_ids["labels"] if isinstance(record_ids, dict) else record_ids
|
|
378
|
+
ids = [list(lab)] if single else [list(x) for x in lab]
|
|
379
|
+
per, budget = {}, {}
|
|
380
|
+
for c, j in enumerate(period_ids):
|
|
381
|
+
n = len(Y[c])
|
|
382
|
+
ic = _positions(cal[c], n, None if ids is None else ids[c], "calibration", j, fixed_split_by)
|
|
383
|
+
ie = _positions(ev[c], n, None if ids is None else ids[c], "evaluation", j, fixed_split_by)
|
|
384
|
+
both = np.intersect1d(ic, ie)
|
|
385
|
+
if both.size:
|
|
386
|
+
r = both[0] if ids is None else ids[c][both[0]]
|
|
387
|
+
raise ValueError(f"period {j}: {both.size} record(s) in both the calibration and the evaluation set "
|
|
388
|
+
f"(e.g. {r!r}); the two sets must be disjoint")
|
|
389
|
+
if ic.size == 0 or ie.size == 0:
|
|
390
|
+
raise ValueError(f"period {j}: the calibration and the evaluation set must both be non-empty")
|
|
391
|
+
lv = {}
|
|
392
|
+
for side, P in (("ref", P_ref[c]), ("final", P_fin[c])):
|
|
393
|
+
th = fit_exact(P[ic], Y[c][ic])
|
|
394
|
+
lv["V_" + side] = macro_f1(P[ie], Y[c][ie], th)
|
|
395
|
+
lv["U_" + side] = macro_f1(P[ie], Y[c][ie], rule)
|
|
396
|
+
per[j] = lv
|
|
397
|
+
budget[j] = dict(records=n, calibration_records=int(ic.size), evaluation_records=int(ie.size),
|
|
398
|
+
unused_records=int(n - ic.size - ie.size),
|
|
399
|
+
calibration_positives_per_label=[int(v) for v in Y[c][ic].sum(0)])
|
|
400
|
+
|
|
401
|
+
def block(levels):
|
|
402
|
+
Ur, Uf, Vr, Vf = levels["U_ref"], levels["U_final"], levels["V_ref"], levels["V_final"]
|
|
403
|
+
D, A = Ur - Uf, Vr - Vf
|
|
404
|
+
Rr, Rf = Vr - Ur, Vf - Uf
|
|
405
|
+
comp = dict(deficit=D, attainable_gap=A, mismatch_change=Rf - Rr, mismatch_ref=Rr, mismatch_final=Rf)
|
|
406
|
+
res = abs(D - (A + comp["mismatch_change"]))
|
|
407
|
+
if res > IDENTITY_TOL:
|
|
408
|
+
raise AssertionError(f"deficit != attainable gap + change in mismatch: {res:.3g}")
|
|
409
|
+
return dict(carried=dict(ref=Ur, final=Uf, convention="U on the evaluation records"),
|
|
410
|
+
refitted=dict(ref=Vr, final=Vf, convention=FIXED_SPLIT_LABEL), components=comp)
|
|
411
|
+
|
|
412
|
+
mean = {k: float(np.mean([per[j][k] for j in period_ids])) for k in per[period_ids[0]]}
|
|
413
|
+
out = block(mean)
|
|
414
|
+
out["per_period"] = {j: block(per[j]) for j in period_ids}
|
|
415
|
+
K = Y[0].shape[1]
|
|
416
|
+
lab_ = (lambda k: names[k]) if names is not None else (lambda k: k)
|
|
417
|
+
out["metadata"] = dict(
|
|
418
|
+
rule_name=rule_name, rule=rule.tolist(), label_ids=names, n_labels=K, period_ids=period_ids,
|
|
419
|
+
single_period=single, seed=None, splits=None, carried_level="U on the evaluation records",
|
|
420
|
+
label_budget=budget, label_budget_note="labelled records the refit is fitted on (calibration), per period",
|
|
421
|
+
ties=TIE_CONVENTION, absent_labels=ABSENT_LABEL_CONVENTION,
|
|
422
|
+
labels_without_positive={j: v for j, v in
|
|
423
|
+
{j: [lab_(k) for k in range(K) if Y[c][:, k].sum() == 0]
|
|
424
|
+
for c, j in enumerate(period_ids)}.items() if v},
|
|
425
|
+
score_scale=scale, signs=SIGNS, shared_terms="not computed under a fixed calibration split",
|
|
426
|
+
estimator=FIXED_SPLIT_LABEL + " (fitters.fit_exact on the calibration records, macro_f1 on the evaluation "
|
|
427
|
+
"records; periods averaged uniformly)",
|
|
428
|
+
package_version=version)
|
|
429
|
+
out["decomposition"] = None
|
|
430
|
+
out["evaluations"] = None
|
|
431
|
+
return out
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
# ------------------------------------------------------------------ method contrast ---
|
|
435
|
+
def _per_partition(ev, key, rule, cols, w):
|
|
436
|
+
X = ev.sep if key == "V_sep" else ev.U_half[rule]
|
|
437
|
+
H = 0.5 * (X[:, 0, cols] + X[:, 1, cols])
|
|
438
|
+
return H @ w
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
TERMS = ("lead_at_carried_rule", "lead_after_refit", "placement")
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def method_contrast(evals_a, evals_b, rule, *, periods=None, weights=None, halves=False, names=("a", "b"),
|
|
445
|
+
seeds=None, same_rule=True):
|
|
446
|
+
"""{seed: Evaluation} for methods a and b, and the carried rule's name -> leads of a over b
|
|
447
|
+
at the carried rule (G_U), after both refit (G_V), and the change after refitting
|
|
448
|
+
(change = G_V - G_U, as in the paper), each with mean, 95 % t interval over seeds and
|
|
449
|
+
unadjusted class. Positive means a is ahead. "placement" (G_U - G_V) is the negative of
|
|
450
|
+
the change, kept for compatibility. The spread over
|
|
451
|
+
partitions within each seed is reported apart and never added to the seed interval.
|
|
452
|
+
same_rule=False lets each method carry its own vector under that name."""
|
|
453
|
+
if set(evals_a) != set(evals_b) and seeds is None:
|
|
454
|
+
raise ValueError(f"the two methods hold different seeds ({sorted(evals_a)} against {sorted(evals_b)})")
|
|
455
|
+
seeds = sorted(evals_a) if seeds is None else sorted(int(s) for s in seeds)
|
|
456
|
+
per_seed, vals, spread = {}, {t: {} for t in TERMS}, {t: {} for t in TERMS}
|
|
457
|
+
for s in seeds:
|
|
458
|
+
a, b = evals_a[s], evals_b[s]
|
|
459
|
+
d = decompose(a, b, rule, periods=periods, weights=weights, halves=halves, same_rule=same_rule)
|
|
460
|
+
per_seed[s] = d
|
|
461
|
+
vals["lead_at_carried_rule"][s], vals["lead_after_refit"][s], vals["placement"][s] = d["D"], d["A"], d["R"]
|
|
462
|
+
P, cols = a._cols(periods)
|
|
463
|
+
ww = weights
|
|
464
|
+
if ww is None and a.shared_weights is not None and P == list(a.shared_periods):
|
|
465
|
+
ww = a.shared_weights # summary()'s default on the shared periods
|
|
466
|
+
w = np.full(len(cols), 1.0 / len(cols)) if ww is None else np.asarray(ww, float) / np.sum(ww)
|
|
467
|
+
u = _per_partition(a, "U", rule, cols, w) - _per_partition(b, "U", rule, cols, w)
|
|
468
|
+
v = _per_partition(a, "V_sep", rule, cols, w) - _per_partition(b, "V_sep", rule, cols, w)
|
|
469
|
+
sd = lambda x: float(np.std(x, ddof=1)) if len(x) > 1 else None
|
|
470
|
+
spread["lead_at_carried_rule"][s], spread["lead_after_refit"][s], spread["placement"][s] = (
|
|
471
|
+
sd(u),
|
|
472
|
+
sd(v),
|
|
473
|
+
sd(u - v),
|
|
474
|
+
)
|
|
475
|
+
U_a = {s: per_seed[s]["U_ref"] for s in seeds}
|
|
476
|
+
U_b = {s: per_seed[s]["U_final"] for s in seeds}
|
|
477
|
+
V_a = {s: per_seed[s]["V_ref"] for s in seeds}
|
|
478
|
+
V_b = {s: per_seed[s]["V_final"] for s in seeds}
|
|
479
|
+
terms = dict(lead_at_carried_rule=inference.paired_contrast(U_a, U_b, seeds=seeds),
|
|
480
|
+
lead_after_refit=inference.paired_contrast(V_a, V_b, seeds=seeds),
|
|
481
|
+
# the paired difference of the two leads, seed by seed
|
|
482
|
+
placement=inference.paired_contrast(vals["lead_at_carried_rule"], vals["lead_after_refit"],
|
|
483
|
+
seeds=seeds))
|
|
484
|
+
for t in TERMS:
|
|
485
|
+
terms[t]["per_seed"] = {s: vals[t][s] for s in seeds}
|
|
486
|
+
# the change after refitting, refitted lead minus carried lead (the negative of placement)
|
|
487
|
+
vals["change"] = {s: vals["lead_after_refit"][s] - vals["lead_at_carried_rule"][s] for s in seeds}
|
|
488
|
+
terms["change"] = inference.paired_contrast(vals["lead_after_refit"], vals["lead_at_carried_rule"], seeds=seeds)
|
|
489
|
+
terms["change"]["per_seed"] = dict(vals["change"])
|
|
490
|
+
spread["change"] = dict(spread["placement"])
|
|
491
|
+
m_rule = terms["lead_at_carried_rule"]["mean"]
|
|
492
|
+
return dict(direction=f"{names[0]} minus {names[1]} (positive: {names[0]} scores higher)", names=list(names),
|
|
493
|
+
rule=rule, same_rule_per_seed={s: per_seed[s]["same_rule"] for s in seeds},
|
|
494
|
+
periods=per_seed[seeds[0]]["periods"], carried_level=per_seed[seeds[0]]["U"],
|
|
495
|
+
seeds=seeds, terms=terms,
|
|
496
|
+
placement_share=(terms["placement"]["mean"] / m_rule if m_rule != 0 else None),
|
|
497
|
+
training_run_uncertainty=("seed (training run): one value per seed, paired across methods; mean, "
|
|
498
|
+
"marginal 95% t interval over seeds, unadjusted class"),
|
|
499
|
+
record_split_variation=dict(
|
|
500
|
+
values=spread,
|
|
501
|
+
unit=("partition (record split) within one run: SD over the partitions of the per-partition "
|
|
502
|
+
"value, carried level on the held-out halves; not a standard error of record sampling "
|
|
503
|
+
"and not combined with the seed interval")),
|
|
504
|
+
per_seed=per_seed)
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: refitgap
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Split a continual learner's thresholded forgetting into what the scores lost and what the carried rule lost, from stored scores.
|
|
5
|
+
Author-email: Hisham Ihshaish <hisham.ihshaish@uwe.ac.uk>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/ihshaish/refitgap
|
|
8
|
+
Project-URL: Repository, https://github.com/ihshaish/refitgap
|
|
9
|
+
Project-URL: Issues, https://github.com/ihshaish/refitgap/issues
|
|
10
|
+
Keywords: continual learning,multi-label classification,backward transfer,thresholds,evaluation
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Description-Content-Type: text/plain
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: numpy>=1.24
|
|
26
|
+
Requires-Dist: scipy>=1.10
|
|
27
|
+
Provides-Extra: plots
|
|
28
|
+
Requires-Dist: matplotlib>=3.5; extra == "plots"
|
|
29
|
+
Provides-Extra: test
|
|
30
|
+
Requires-Dist: pytest>=7.0; extra == "test"
|
|
31
|
+
Requires-Dist: scikit-learn>=1.3; extra == "test"
|
|
32
|
+
Dynamic: license-file
|
|
33
|
+
|
|
34
|
+
refitgap
|
|
35
|
+
========
|
|
36
|
+
|
|
37
|
+
Train a multi-label classifier period by period, score the final model on an old period, and
|
|
38
|
+
its macro-F1 there drops. Part of the drop sits in the scores, which now separate the labels
|
|
39
|
+
less well, and part in the thresholds, carried from period one and now in the wrong place.
|
|
40
|
+
refitgap says how much of each, from stored scores alone:
|
|
41
|
+
|
|
42
|
+
deficit B = attainable gap A + change in mismatch dR
|
|
43
|
+
|
|
44
|
+
The attainable gap compares the two models after each refits its thresholds on equal terms
|
|
45
|
+
(split-half cross-fitting, same partitions); the change in mismatch is what each loses by
|
|
46
|
+
keeping the carried rule. Same trick for two methods: replay's lead over sequential training
|
|
47
|
+
at the inherited thresholds, after both refit, and the change. This is the code of
|
|
48
|
+
"Decomposing Backward Transfer in Continual Multi-Label Text Classification" (Ihshaish,
|
|
49
|
+
Mayhew and Aydin, under review), with its plans, stored results and scripts.
|
|
50
|
+
|
|
51
|
+
Install
|
|
52
|
+
-------
|
|
53
|
+
|
|
54
|
+
pip install refitgap # Python 3.10+, numpy and scipy, nothing else
|
|
55
|
+
pip install "refitgap[plots,test]" # matplotlib for figures, pytest etc. for the tests
|
|
56
|
+
|
|
57
|
+
(or pip install . from the clone). The entry points below read files in this repository, so
|
|
58
|
+
clone it too. On the command line, refitgap evaluate | report | example runs the same
|
|
59
|
+
evaluator on stored arrays (refitgap --help; API.txt, "Command line").
|
|
60
|
+
|
|
61
|
+
Reproduce Figure 2
|
|
62
|
+
------------------
|
|
63
|
+
|
|
64
|
+
python examples/figure2/reproduce_figure2.py # a few minutes on 4 cores
|
|
65
|
+
python examples/figure2/reproduce_figure2.py --from-levels # about a second
|
|
66
|
+
|
|
67
|
+
All five paired comparisons of Figure 2 (supplement Table C.1) over periods 2 to 7: lead at
|
|
68
|
+
the inherited thresholds, after both refit, and the change. Paired t intervals over six
|
|
69
|
+
DistilBERT pairs (from stored per-pair levels: their 30 score files, about 60 MB, do not ship),
|
|
70
|
+
percentile bootstrap over ten BiGRU pairs (from raw scores in paper/results/, or stored levels
|
|
71
|
+
with --from-levels). Every lead, mean and bound must match examples/figure2/expected.json to
|
|
72
|
+
1e-10.
|
|
73
|
+
|
|
74
|
+
python examples/two_scorer/table1.py
|
|
75
|
+
|
|
76
|
+
Table 1: two scorers with equal AUROC (6/11), average precision (5/18) and deficit (1/26),
|
|
77
|
+
one all change in mismatch, the other all attainable gap. Exact fractions, no luck involved!
|
|
78
|
+
|
|
79
|
+
Compare your own models
|
|
80
|
+
-----------------------
|
|
81
|
+
|
|
82
|
+
python examples/your_models/compare_your_models.py [DATA_DIR] --a mine --b baseline
|
|
83
|
+
|
|
84
|
+
DATA_DIR holds three kinds of CSV (examples/your_models/data/ is a tiny synthetic set):
|
|
85
|
+
|
|
86
|
+
labels.csv period, record_id, one 0/1 column per label
|
|
87
|
+
scores_<method>_seed<k>.csv period, record_id, one score column per label (the final
|
|
88
|
+
model of <method>, trained under seed k)
|
|
89
|
+
thresholds.csv method, seed, the carried rule (one threshold per label)
|
|
90
|
+
|
|
91
|
+
Out come a's leads over b at the carried rule and after both refit, and the change (refitted
|
|
92
|
+
lead minus carried lead), per seed and with paired t intervals. In Python: refitgap.compare()
|
|
93
|
+
and method_contrast(); API.txt has the lot.
|
|
94
|
+
|
|
95
|
+
Interface
|
|
96
|
+
---------
|
|
97
|
+
|
|
98
|
+
In: aligned scores and labels, the carried rule, and a seed for the common partitions.
|
|
99
|
+
- Scores: one (records, labels) array per period and model, probabilities in [0, 1]; logits go
|
|
100
|
+
through a sigmoid first, or declare score_scale="other" with thresholds on the same scale.
|
|
101
|
+
Labels: 0/1, same shape.
|
|
102
|
+
- Alignment: rows match by record id, columns by label name. A shape mismatch, a duplicate or
|
|
103
|
+
missing id, a different label set, or different labels for the two models raises. Nothing
|
|
104
|
+
is truncated or dropped quietly.
|
|
105
|
+
- Ties: a score equal to its threshold is a positive. A refitted threshold is an observed
|
|
106
|
+
score; of two equally good cutpoints the higher wins.
|
|
107
|
+
- A label with no positive in a period has F1 0 there and stays in the macro mean.
|
|
108
|
+
- Partitions: partition b of period id j under seed s is the b-th permutation of numpy
|
|
109
|
+
default_rng(7000 + 100*s + j); first half fits, second scores, then the reverse. 200 in the
|
|
110
|
+
paper. Both models of a pair share them, so give both the same seed and period ids. Period
|
|
111
|
+
ids are integers in [0, 100); the paper's periods 2 to 7 are ids 1 to 6.
|
|
112
|
+
- Out: carried level U, refitted level V, deficit, attainable gap and change in mismatch, per
|
|
113
|
+
period and averaged; for two methods, leads per seed with means and 95% intervals. Positive
|
|
114
|
+
means the first-named model scores higher. Numbers only: refitgap recommends nothing, and
|
|
115
|
+
the attainable gap describes the scores, not what the representation kept.
|
|
116
|
+
|
|
117
|
+
Repository layout
|
|
118
|
+
-----------------
|
|
119
|
+
|
|
120
|
+
src/refitgap/ the evaluator; src/refitgap/API.txt is the reference
|
|
121
|
+
examples/ Table 1, Figure 2, your own models, walkthrough.ipynb (seconds, the lot)
|
|
122
|
+
paper/ plans, scripts, stored results, checks; README.txt maps tables to scripts,
|
|
123
|
+
DATA.txt the corpora (public, not redistributed, bar a 3200-record ASRS sample)
|
|
124
|
+
paper/threshold_policy_survey.txt how 34 sampled studies report their thresholds
|
|
125
|
+
tests/ python -m pytest (fast, a minute or two); -m slow adds the full Figure 2
|
|
126
|
+
recomputation from raw scores (minutes)
|
|
127
|
+
|
|
128
|
+
MIT licence. Please cite the paper and the software (CITATION.cff has both). Have fun with it!
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
refitgap/API.txt,sha256=29L4hrWvbfQaRfAzzvfvQIWakdFIvyne61Sgz-Ejp4A,9013
|
|
2
|
+
refitgap/__init__.py,sha256=wM4y6Q8u676qK_ml5gmp_hcHIn0xjpt5bi4vFzH7_gI,1898
|
|
3
|
+
refitgap/__main__.py,sha256=XRlT-v5jT-bYKqtjP2qc3yS7Q7xtPQ9oXMUcv-f_qiA,109
|
|
4
|
+
refitgap/analysis.py,sha256=qDyyg3wol7fabBgImxwQyujnyglGdDxNOikG77vWYDI,22542
|
|
5
|
+
refitgap/cli.py,sha256=WON-IjeaHOv77-VB8ELIdKxXIzqj7_jj_N-yKudXWOU,8982
|
|
6
|
+
refitgap/crossfit.py,sha256=H1z0IE3m8Q6mdaBrYBKNDuJyd3gRYZo1DpVqgq4DuR8,15603
|
|
7
|
+
refitgap/example.py,sha256=YVY1FRFjZD9f9Bt71H74Ccan2Ne8sVIYeYAD8Zn7KWw,5543
|
|
8
|
+
refitgap/fitters.py,sha256=lnO3EtYhkWQKYG3JkBguI_q9xX6jqdF_neUxJPy7f78,4859
|
|
9
|
+
refitgap/identification.py,sha256=hbPIAk9gMRAX5RJZB0Rvx2Nx3Q6uwBCETS3pmsvoI6s,4656
|
|
10
|
+
refitgap/inference.py,sha256=j5Wy9F1sqFrFcd9nEfk2HWFZLiCmIHQQTnqt34ZB1mI,13017
|
|
11
|
+
refitgap/plots.py,sha256=WgJ1FTI5iq2pb575pLWL0NU9CL_kbmWtJAnJhMYW67Q,10342
|
|
12
|
+
refitgap/records.py,sha256=j-9hLmYLHWJkQmtO0Y7bjJVOP5sXPwNs6LX4DU-grQ4,1736
|
|
13
|
+
refitgap/report.py,sha256=-CYbz8kJqrZqHQxqAzO7Jwt8l9cb8xBk2ug1JYo5aeU,7779
|
|
14
|
+
refitgap/runs.py,sha256=eAYMlRAc7sXLEEOZ8oVb50qBSVhwmoDEJaVu7A8IPqA,2574
|
|
15
|
+
refitgap/tiny.py,sha256=ulnnjq7enQ26palcmFudeqzWTA3gCe0ONfnWHwHx1I8,3094
|
|
16
|
+
refitgap/twoscorer.py,sha256=fy8V9E94u38MudKP9c1-sZ6sdnpayh3wI2suQKT8lJA,30205
|
|
17
|
+
refitgap-0.1.0.dist-info/licenses/LICENSE,sha256=B-C9c9N9lYv8ici8_SmsMRawSBawjXFGa8jOz8HuGHs,1072
|
|
18
|
+
refitgap-0.1.0.dist-info/METADATA,sha256=duMlRZohkab3Kg98ZsEF3_Ta1W_O7il8drLISSKUAt4,6792
|
|
19
|
+
refitgap-0.1.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
20
|
+
refitgap-0.1.0.dist-info/entry_points.txt,sha256=PAR24gjBRksWXkQ61Y_PouV5G1ygjjdrTONsbnNLpTA,47
|
|
21
|
+
refitgap-0.1.0.dist-info/top_level.txt,sha256=sgW0kITfI4B4AnoPF5g9O3IqvGaCqSyhKYmmaWDcOJg,9
|
|
22
|
+
refitgap-0.1.0.dist-info/RECORD,,
|