Thibaut Forest Claude Fable 5 commited on
Commit
edd5d5e
·
1 Parent(s): 6627f14

Phase 52: building-grouped CV adopted - screen gains leave-building-out column; contamination gradient rf +.194 (scenario) -> ~0 (building); logbag choice confirmed, no reversal

Browse files
Files changed (2) hide show
  1. EXPERIMENTS.md +43 -0
  2. batteryswap_example/cv.py +53 -11
EXPERIMENTS.md CHANGED
@@ -2644,3 +2644,46 @@ catch. It caught ZERO. The local -37.9 was wobbler catches that the public
2644
  fleet does not offer below the cut; a guard cannot create catches that are
2645
  not there. Cert-first family closed entirely - the label-lag population
2646
  either does not exist on the scored fleets or is already inside the head.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2644
  fleet does not offer below the cut; a guard cannot create catches that are
2645
  not there. Cert-first family closed entirely - the label-lag population
2646
  either does not exist on the scored fleets or is already inside the head.
2647
+
2648
+ ## Phase 52 - building-grouped CV adopted; the contamination gradient quantified
2649
+
2650
+ Trigger: a sweep of every public competitor artifact (HF repos, hidden HF
2651
+ branches, GitHub repos, and deleted-HF-repo cards recovered via the
2652
+ 686f6c61/radar-huggingface-dataset archive) established that the scored
2653
+ public/private fleets hold FOREIGN BUILDINGS: the Attention-Heads card states
2654
+ it outright, and the FAQ transcription in ziadnasif77-debug's repo puts the
2655
+ full pool at ~200 buildings vs train's 24. Both serious hidden teams
2656
+ (YassY-The-AlchemYst's RobustHazardPlanner lineage, ziad) validate and
2657
+ calibrate building-grouped. Organizer-shaped conclusion, user directive:
2658
+ the trusted offline fold must be leave-BUILDING-out.
2659
+
2660
+ Change (cv.py): `screen` gains a third grouping column, whole buildings
2661
+ assigned to 5 folds greedily balanced by unique observed deaths (deaths are
2662
+ lumpy - the top building holds 15/82, nine hold exactly one; random building
2663
+ assignment starves folds). `static`/`ablate` already split by building
2664
+ (fleet_halves), so this closes the last battery-grouped selection surface.
2665
+ Selection guidance in the module now reads: building > battery > scenario.
2666
+
2667
+ Result, 3 fold-assignment seeds - rec12 margin over logistic (AUC margins
2668
+ agree in sign; levels are NOT comparable across columns, margins are the
2669
+ decision quantity):
2670
+
2671
+ | model | scenario | battery (s0/s1/s2) | building (s0/s1/s2) |
2672
+ |---|---|---|---|
2673
+ | logbag | +.003 | -.006 / -.004 / +.001 | -.002 / +.000 / +.000 |
2674
+ | rf | **+.194** | +.023 / +.031 / -.008 | +.005 / -.005 / -.014 |
2675
+ | gbm | **+.148** | -.000 / -.016 / -.026 | -.022 / -.039 / -.030 |
2676
+
2677
+ The contamination gradient is monotone: rf's +.194 phantom margin on
2678
+ scenario folds collapses to ~+.02 on battery folds and to zero-or-negative
2679
+ on building folds; gbm goes outright negative. The residual rf margin in the
2680
+ battery column (sign-unstable across seeds) is sibling leakage - a held-out
2681
+ device's building/batch mates stay in train. logbag == logistic (margin ~0)
2682
+ in every column and every seed: the champion's model choice never rested on
2683
+ fold luck, and the deployment-shaped fold CONFIRMS it rather than reversing
2684
+ anything. No selection changes; the protocol does.
2685
+
2686
+ Standing rule upgrade (supersedes the blind-loop "group by battery" rule):
2687
+ **group by BUILDING when the deployment is cross-building.** Battery
2688
+ grouping is necessary but not sufficient; it still leaks the environment/
2689
+ install-batch channel that phase 10/13 closed on the feature side.
batteryswap_example/cv.py CHANGED
@@ -12,15 +12,19 @@ real leaderboard points to learn.
12
  median rank of missed deaths beside the cost. Implemented in
13
  `models_cv.rank_stats`, so `dev`/`panel`/`test` get it too.
14
 
15
- **2. Group by BATTERY, not by scenario or row.**
16
  Each battery contributes one near-duplicate row per closed origin (~3600
17
  rows from ~460 batteries), so holding out a SCENARIO leaves the same
18
  batteries in train and test and tree models score their own memorised
19
  signatures. Measured in the blind loop: leave-scenario-out put HGB ahead of
20
  linear (AUC .9907 vs .9843); leave-battery-out REVERSED it (.9769 vs .9845)
21
  and the cost harness agreed with the clean split. The effective sample size
22
- is ~460, not ~3600. `screen` runs both groupings side by side so the gap
23
- stays visible.
 
 
 
 
24
 
25
  **3. The deployment gate: would this fire in the static branch?**
26
  The scored splits are fleets the model has never seen; on them `submit.py`
@@ -205,14 +209,44 @@ def run_static(variant='base', seed=0, origins=range(48),
205
 
206
 
207
  # --------------------------------------------------------------------------
208
- # protocol 3: offline model screen, grouped by battery
209
  # --------------------------------------------------------------------------
210
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
211
  def screen(kinds=('logistic', 'logbag', 'rf', 'gbm'), n_folds=5, seed=0,
212
  dataset_path=Path('dataset')):
213
- """Leave-BATTERY-out vs leave-SCENARIO-out on the same pairs and the same
214
- models. The gap between the two columns is the contamination; trust the
215
- battery column. Headline is recall at the operating N, AUC secondary."""
 
 
 
216
  from sklearn.metrics import roc_auc_score
217
 
218
  t = load_table(dataset_path)
@@ -223,14 +257,20 @@ def screen(kinds=('logistic', 'logbag', 'rf', 'gbm'), n_folds=5, seed=0,
223
  dev_fold = pd.Series(rng.permutation(len(devs)) % n_folds, index=devs)
224
  org = np.sort(closed['origin_idx'].unique())
225
  org_fold = pd.Series(np.arange(len(org)) % n_folds, index=org)
 
 
 
 
226
 
227
  n_dev_pos = closed.loc[closed['dies42'] == 1, 'device_id'].nunique()
228
  print(f'pairs {len(closed)} batteries {len(devs)} origins {len(org)} '
 
229
  f'positives {int(closed["dies42"].sum())} rows / {n_dev_pos} batteries')
230
  print('effective n is the BATTERY count, not the row count - regularisation '
231
  'tuned against rows is ~8x too weak here.\n')
232
 
233
- schemes = {'battery': closed['device_id'].map(dev_fold),
 
234
  'scenario': closed['origin_idx'].map(org_fold)}
235
  out = {}
236
  for scheme, fold in schemes.items():
@@ -279,9 +319,11 @@ def screen(kinds=('logistic', 'logbag', 'rf', 'gbm'), n_folds=5, seed=0,
279
  a0, r0, _ = out[(s, ref)]
280
  cells += [f'{a - a0:+16.4f}', f'{r - r0:+16.3f}']
281
  print(f'{kind:10s} ' + ' '.join(cells))
282
- print('\nA model whose margin shrinks or reverses when the grouping goes '
283
- 'from scenario to battery was reading memorised battery identity. '
284
- 'Selection follows the battery column.')
 
 
285
  return out
286
 
287
 
 
12
  median rank of missed deaths beside the cost. Implemented in
13
  `models_cv.rank_stats`, so `dev`/`panel`/`test` get it too.
14
 
15
+ **2. Group by BUILDING > battery > scenario.**
16
  Each battery contributes one near-duplicate row per closed origin (~3600
17
  rows from ~460 batteries), so holding out a SCENARIO leaves the same
18
  batteries in train and test and tree models score their own memorised
19
  signatures. Measured in the blind loop: leave-scenario-out put HGB ahead of
20
  linear (AUC .9907 vs .9843); leave-battery-out REVERSED it (.9769 vs .9845)
21
  and the cost harness agreed with the clean split. The effective sample size
22
+ is ~460, not ~3600. And battery grouping is still not the deployment split:
23
+ the scored public/private fleets hold FOREIGN BUILDINGS (organizer design;
24
+ also how `static` splits), and siblings in one building share install batch
25
+ and thermal environment, so a battery fold keeps a held-out device's
26
+ building-mates in train. `screen` runs all three groupings side by side;
27
+ selection follows the BUILDING column (2026-08-22 protocol change).
28
 
29
  **3. The deployment gate: would this fire in the static branch?**
30
  The scored splits are fleets the model has never seen; on them `submit.py`
 
209
 
210
 
211
  # --------------------------------------------------------------------------
212
+ # protocol 3: offline model screen, grouped by building > battery
213
  # --------------------------------------------------------------------------
214
 
215
+ def building_folds(closed, dev_bldg, n_folds=5, seed=0):
216
+ """Assign whole BUILDINGS to folds, balancing observed deaths.
217
+
218
+ Deaths are lumpy across the 24 buildings (top building holds 15/82, nine
219
+ hold exactly one), so a random building assignment routinely starves a
220
+ fold of positives. Greedy balance instead: buildings in descending
221
+ unique-death order, each to the death-lightest fold so far (ties broken
222
+ by device load, then by the seed's shuffle).
223
+ """
224
+ per_dev = (closed[['device_id', 'dies42']]
225
+ .assign(bldg=closed['device_id'].map(dev_bldg).values)
226
+ .groupby('device_id').agg(bldg=('bldg', 'first'),
227
+ died=('dies42', 'max')))
228
+ per_b = per_dev.groupby('bldg').agg(devices=('died', 'size'),
229
+ deaths=('died', 'sum'))
230
+ order = (per_b.sample(frac=1, random_state=np.random.RandomState(seed))
231
+ .sort_values(['deaths', 'devices'], ascending=False, kind='stable'))
232
+ deaths_in, devs_in = np.zeros(n_folds), np.zeros(n_folds)
233
+ fold_of = {}
234
+ for b, row in order.iterrows():
235
+ f = int(np.lexsort((devs_in, deaths_in))[0])
236
+ fold_of[b] = f
237
+ deaths_in[f] += row['deaths']
238
+ devs_in[f] += row['devices']
239
+ return pd.Series(fold_of, name='fold')
240
+
241
+
242
  def screen(kinds=('logistic', 'logbag', 'rf', 'gbm'), n_folds=5, seed=0,
243
  dataset_path=Path('dataset')):
244
+ """Leave-BUILDING-out vs leave-BATTERY-out vs leave-SCENARIO-out on the
245
+ same pairs and the same models. The gaps between columns are contamination;
246
+ trust the BUILDING column - the scored public/private fleets hold foreign
247
+ buildings, and building-mates share install batch and thermal environment,
248
+ so battery folds still leak sibling identity. Headline is recall at the
249
+ operating N, AUC secondary."""
250
  from sklearn.metrics import roc_auc_score
251
 
252
  t = load_table(dataset_path)
 
257
  dev_fold = pd.Series(rng.permutation(len(devs)) % n_folds, index=devs)
258
  org = np.sort(closed['origin_idx'].unique())
259
  org_fold = pd.Series(np.arange(len(org)) % n_folds, index=org)
260
+ dev_bldg = (pd.read_csv(dataset_path / 'train' / 'devices.csv')
261
+ .drop_duplicates('device_id')
262
+ .set_index('device_id')['building_id'])
263
+ bldg_fold = building_folds(closed, dev_bldg, n_folds, seed)
264
 
265
  n_dev_pos = closed.loc[closed['dies42'] == 1, 'device_id'].nunique()
266
  print(f'pairs {len(closed)} batteries {len(devs)} origins {len(org)} '
267
+ f'buildings {bldg_fold.size} '
268
  f'positives {int(closed["dies42"].sum())} rows / {n_dev_pos} batteries')
269
  print('effective n is the BATTERY count, not the row count - regularisation '
270
  'tuned against rows is ~8x too weak here.\n')
271
 
272
+ schemes = {'building': closed['device_id'].map(dev_bldg).map(bldg_fold),
273
+ 'battery': closed['device_id'].map(dev_fold),
274
  'scenario': closed['origin_idx'].map(org_fold)}
275
  out = {}
276
  for scheme, fold in schemes.items():
 
319
  a0, r0, _ = out[(s, ref)]
320
  cells += [f'{a - a0:+16.4f}', f'{r - r0:+16.3f}']
321
  print(f'{kind:10s} ' + ' '.join(cells))
322
+ print('\nA model whose margin shrinks or reverses as the grouping goes '
323
+ 'scenario -> battery -> building was reading memorised identity at '
324
+ 'that level (battery signatures, then building/batch siblings). '
325
+ 'Selection follows the BUILDING column; it is the only one shaped '
326
+ 'like the scored splits, which hold foreign buildings.')
327
  return out
328
 
329