Thibaut Forest Claude Opus 5 (1M context) commited on
Commit
bd56acd
·
1 Parent(s): 3d2958f

Phase 19-20: cold-start count floor (all-48 2146.9->1851.0) + CV harness upgrade

Browse files

Gated floor k=2.5 wins warm-up 8/8, panel byte-identical. Ungated and
foreign-fleet variants refuted (panel +21..+114; fleet-holdout harness
shows the probability scale transfers across device subsets). Near-end
block s_42-47 added: top-up worth -273 there, censor pricing -64.
Three-regime scorecard codified; tie-aware selection stability;
component decomposition by default.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

EXPERIMENTS.md CHANGED
@@ -10,9 +10,29 @@ All numbers are mean `total_cost` per scenario from the official `evaluate_plan`
10
  use only data ≤ origin; the label is death within the 42-day window after it.
11
  - **Walk-forward**: at every scored origin the model is refit on all pairs whose
12
  window *closed* before that origin (fully observed labels, built-in embargo).
13
- - **Blocks**: warm-up s_0–s_7 (discarded: 3–20 known deaths), **dev s_8–s_23**
14
- (16 origins, all tuning), **test s_24–s_41** (18 origins, counted looks only),
15
- s_42–s_47 (discarded: windows extend past data end 2026-07-31).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  - **Reference points** (test block): shipped example 6779 · defer-everything 2512
17
  · lowest-voltage-15 heuristic 2034 · oracle (true EOL) 186.
18
 
@@ -586,6 +606,115 @@ model-curve error rather than real savings. The knob
586
  This completes the phase-17 plan: all five directions adjudicated and
587
  rejected. Champion unchanged: panel 1679.2, all-48 Docker 2146.9.
588
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
589
  ## Test-look ledger
590
 
591
  8 looks total (now historical - see protocol change above); look 9 = the
 
10
  use only data ≤ origin; the label is death within the 42-day window after it.
11
  - **Walk-forward**: at every scored origin the model is refit on all pairs whose
12
  window *closed* before that origin (fully observed labels, built-in embargo).
13
+ - **Current protocol** (since the 2026-08-18 protocol change, after phase 10):
14
+ champion selection on the merged **34-origin panel s_8–s_41** with
15
+ repeated-split stability - accept a challenger only if selected in ≥60% of
16
+ 2000 random half-splits AND modal under blocked splits; the public
17
+ leaderboard is the only remaining holdout and is spent deliberately.
18
+ - **Three-regime scorecard** (since phase 20): the panel alone samples one
19
+ regime (native fleet, mature calibration, expensive false swaps). Standing
20
+ additional blocks: **warm-up s_0–s_7** (cold-start calibration; where the
21
+ phase-19 floor was proven) and **near-end s_42–s_47** (censored shipped
22
+ truth - the same scoring the remote evaluator applies to a held-out split's
23
+ near-end scenarios; where the phase-11/14 mechanisms are measured).
24
+ Adoption of a deployment-regime mechanism requires: no panel regression
25
+ (tie-aware selection stability) AND a win in its target regime block AND
26
+ the regime being reachable by a local harness - "architecturally sound" is
27
+ not sufficient (see the foreign-fleet floor, built and refuted within one
28
+ day in phase 20). selection_stability counts decided splits only;
29
+ summarize() reports late/early/ops components by default.
30
+ - **Other blocks**: warm-up s_0–s_7 (3–16 unique training deaths) is excluded
31
+ from champion selection but scored since phase 17 for cold-start behavior;
32
+ s_42–s_47 stay unscored (windows extend past data end 2026-07-31).
33
+ - **Historical blocks (phases 1–10)**: dev s_8–s_23 (all tuning) / test
34
+ s_24–s_41 (counted looks only, ledger at the end); merged once 8 looks had
35
+ spent the test block. Per-phase "dev"/"test" columns below refer to these.
36
  - **Reference points** (test block): shipped example 6779 · defer-everything 2512
37
  · lowest-voltage-15 heuristic 2034 · oracle (true EOL) 186.
38
 
 
606
  This completes the phase-17 plan: all five directions adjudicated and
607
  rejected. Champion unchanged: panel 1679.2, all-48 Docker 2146.9.
608
 
609
+ ## Phase 17 - late-swap forensics: the misses are mostly NOT silent plungers (cold-start floor ADOPTED)
610
+
611
+ Question: which batteries get replaced too late? Attributed the all-48
612
+ submission's late cost through the evaluator's own transitions (its emergency
613
+ loop books missed deaths as late_swap at 10/day, day ~43+):
614
+
615
+ - 98% of late cost (73,430 of 74,850) is 263 parked deaths swapped as
616
+ post-window emergencies; only 1,420 is caught-but-late (deaths on days 1-7
617
+ swapped a few days after).
618
+ - State at origin of the 263: **157 already below 2.45** (48,460 cost) · 71
619
+ in visible droop 2.45-2.55 (17,710) · only 35 healthy+fresh (7,260). The
620
+ phase-8-13 "silent plunger wall" is ~10% of the late cost, not the story.
621
+ - (The repo submission.csv was the phase-5-era artifact, all-48 2202; all
622
+ numbers here use same-code regenerations.)
623
+
624
+ Mechanism 1 - **cold-start count starvation** (s_0-s_8 = 46% of all-48 late):
625
+ at warm-up origins the ranking is fine (AUC 0.97 at s_0; missed deaths at
626
+ ranks 2-8) but sum(p42) undercounts prevalence ~4x (3-16 unique training
627
+ deaths, lower-prevalence era), so n = 0.8*sum(p42) includes 1-8 batteries
628
+ while 9-19 die - catch rate 0.00/0.00/0.06 at s_0/1/2. Invisible to every
629
+ prior selection round: CV discards s_0-s_7 as warm-up. Cold-start late
630
+ ~700/scenario ~= the public-LB late gap to the leader (1296 vs 660) -
631
+ phase 10's "not recoverable" verdict was derived on the test block only.
632
+
633
+ Mechanism 2 - **label-lag suppression** (bulk of the remainder): long-crossed
634
+ devices (59-275d below 2.45) score p42 0.001-0.3 - the phase-15 load-bearing
635
+ negatives working as trained; the minority whose EOL does arrive in-window
636
+ pays 300-500 each. Still open; LogbagLAH (KM crossing->EOL arrival curve) is
637
+ the drafted candidate.
638
+
639
+ Fix for mechanism 1 (ADOPTED): **gated inclusion floor** in plan_tsroute -
640
+ psum_eff = max(sum(p42), k * prev_est), with prev_est = max(deaths recorded
641
+ in the last 42d, fresh devices first crossing 2.45 within the last 42d),
642
+ active only while unique observed training deaths < 20 (true exactly at
643
+ s_0-s_7; the 34-origin panel is byte-identical to the champion by
644
+ construction).
645
+
646
+ | variant | warm-up s_0-s_7 mean |
647
+ |---|---|
648
+ | champion (no floor) | 4256.1 |
649
+ | ungated k=1.0/1.5/2.0 | 3994/3741/3354 - leaks into the panel (+21/+95/+299; recent-deaths overshoots post-peak), rejected by selection stability (champion 89.5%/80%) |
650
+ | gated, recorded-deaths only, k=1.5 | 3740.5 - recording lag blinds the label signal exactly at s_0-s_2 (recent42 = 1 while 9-17 die) |
651
+ | gated + zone signal, k=1.0/1.5/2.0 | 3313/3014/2532 |
652
+ | **gated + zone, k=2.5** | **2379.7** - wins 8/8 origins; k=3.0: 2619, k=4.0: 3191 (interior optimum) |
653
+
654
+ Registered caveat: k and the gate threshold were chosen on the same 8 origins
655
+ (no holdout exists there). Any k in [1.5, 3] recovers 29-44%, so the
656
+ qualitative result is robust; the exact k=2.5 is not.
657
+
658
+ End-to-end verification (submit.py adaptive path, floor vs same-code
659
+ control): s_8-s_47 **byte-identical costs** (gate closed), warm-up 4764.2 ->
660
+ 2976.6 (-37.5%, 8/8 scenarios), **all-48 2146.6 -> 1848.7** (late 1465 ->
661
+ 1125, early 434 -> 515; the control reproduces the recorded Docker 2146.9).
662
+ Champion and panel (1679.2) unchanged. Knobs: BATTERYSWAP_FLOOR (k, 0
663
+ disables the cold-start branch), BATTERYSWAP_FLOOR_GATE. Held-out-split
664
+ caveat: pooled training passes the gate earlier in calendar time there
665
+ (bigger fleet, faster death accrual) and sum(p42) carries the training
666
+ fleet's scale - the foreign-fleet floor branch in submit.py (own_frac gate)
667
+ targets that regime; the local train split cannot exercise it.
668
+
669
+ ## Phase 19 - cold-start count floor (ADOPTED, gated) & ungated variants (rejected)
670
+
671
+ Public resubmission of the phase-14 champion read **1717.8 (rank 3)**: late
672
+ 1296→1234, early +14 - the deployment mechanisms delivered ~-50, so public
673
+ mostly does NOT sit in the cheap-FP regime, and the remaining -240 to the
674
+ leader is late-swap conversion. Cold-start diagnosis (warm-up block): at
675
+ s_0-s_7 the model RANKS deaths at the very top (AUC 0.97) but sum(p42)
676
+ undercounts prevalence ~4x (3.8 when 17 die) - the count starves, not the
677
+ ranking; 46% of the all-48 late cost. Fix: floor the count with observable
678
+ signals - max(sum_p42, k x max(deaths recorded in last 42d, fresh
679
+ 2.45-crossings)) - gated to fire only while training has seen < 20 unique
680
+ deaths. Warm-up CV: **4256 -> 2380 at k=2.5, winning 8/8 origins**; the
681
+ 34-origin panel is byte-identical (gate closed everywhere).
682
+ All-48 Docker: **2146.9 -> 1851.0** (late 1465→1125).
683
+
684
+ Ungated variants, panel-adjudicated and rejected: k=1.0 recent-only +20.9,
685
+ k=1.0 +zone +75.1, k=1.25 +zone +113.9 (champion 96%/80% selection). Where
686
+ sum(p42) is natively calibrated it beats the empirical death rate (deaths are
687
+ bursty; last-42d != next-42d), and the death-zone signal counts never-dying
688
+ limpers. Sixth refutation of explicit count/label correctors in calibrated
689
+ regimes.
690
+
691
+ ## Phase 20 - CV-harness upgrade: the harness catches a blind ship (foreign floor refuted)
692
+
693
+ Five CV improvements, all implemented:
694
+
695
+ 1. **Fleet-holdout harness** (train the pool on half the devices, plan/score
696
+ the full fleet): built to validate a "foreign-fleet floor" (fire the count
697
+ floor when training positives are mostly another fleet's - the public
698
+ early-origin hypothesis) that had been wired into submit.py on
699
+ architectural grounds. The harness REFUTED it the same day: the
700
+ probability scale transfers across device subsets (half-fleet training:
701
+ sum(p42) 15.8 vs 9.1 true deaths - it OVER-counts if anything); the floor
702
+ was inert on 15/17 origins and lost where it fired (+15.1, 0 wins).
703
+ Branch removed before it could cost a submission. Public late-heaviness is
704
+ therefore NOT foreign-scale starvation.
705
+ 2. **Near-end block s_42-s_47** (censored shipped truth, previously
706
+ discarded): native retro-adjudication of the deployment mechanisms -
707
+ phase-11 top-up worth **-273** (877.6 -> 604.8), phase-14 censoring-aware
708
+ pricing **-64** (669.2 -> 604.8). Now a standing block.
709
+ 3. **Three-regime scorecard** codified in the protocol section.
710
+ 4. **selection_stability** counts decided splits only (all-tie splits no
711
+ longer break alphabetically to the incumbent).
712
+ 5. **summarize()** reports late/early/ops decomposition by default.
713
+
714
+ Champion: logbag / tsroute a0.8 + top-up (m0.8/pm25) + sd7 + censor-aware
715
+ pricing + gated cold-start floor (k=2.5, gate 20). Panel 1679.2 (unchanged),
716
+ warm-up 2380, near-end 604.8, all-48 Docker 1851.0, public 1717.8.
717
+
718
  ## Test-look ledger
719
 
720
  8 looks total (now historical - see protocol change above); look 9 = the
batteryswap_example/models_cv.py CHANGED
@@ -1382,6 +1382,14 @@ def plan_tsroute(pred, included, locations, start_time, day_q,
1382
  pay early-replacement for weeks [floor_day 0 = off]
1383
  """
1384
  psum = pred['p42_sum'].iloc[0] if 'p42_sum' in pred else pred['p42'].sum()
 
 
 
 
 
 
 
 
1385
  n = int(round(plan_tsroute.alpha * psum))
1386
  order_col = 'rank_score' if 'rank_score' in pred.columns else 'p42'
1387
  chosen = pred[order_col].nlargest(n).index
@@ -1450,6 +1458,8 @@ plan_tsroute.floor_day = 0
1450
  plan_tsroute.p_conf = 0.5
1451
  plan_tsroute.ec_margin = 0.0
1452
  plan_tsroute.ec_pmax = 25.0
 
 
1453
 
1454
  def plan_tspig(pred, included, locations, start_time, day_q,
1455
  travel_costs=None, settings=None):
@@ -1657,12 +1667,29 @@ def censored_pairs(table, origin_time):
1657
 
1658
  def walk_forward(table, scenario_ctx, origins, configs):
1659
  """configs: list of dicts(model, planner, p_star, day_q, name)."""
 
 
 
 
1660
  fitted = {}
1661
  records = []
1662
  for idx in origins:
1663
  ctx = scenario_ctx[idx]
1664
  origin_time = ctx['start_time']
 
 
 
 
 
 
1665
  rows = table[table['origin_idx'] == idx].set_index('device_id')
 
 
 
 
 
 
 
1666
 
1667
  preds = {}
1668
  for model_name in {c['model'] for c in configs}:
@@ -1695,6 +1722,16 @@ def walk_forward(table, scenario_ctx, origins, configs):
1695
  PLANNERS['tsroute'].ec_margin = cfg.get('ec_margin', 0.0)
1696
  PLANNERS['tsroute'].ec_pmax = cfg.get('ec_pmax', 25.0)
1697
  PLANNERS['tsroute'].split_days = cfg.get('split_days', 0)
 
 
 
 
 
 
 
 
 
 
1698
  global PARK_MOBILITY
1699
  PARK_MOBILITY = cfg.get('park_mobility', 0.0)
1700
  global EMERGENCY_DAY
@@ -1739,12 +1776,24 @@ def summarize(df, label):
1739
  for key, tag in [('blend', 'b'), ('p_be', 'be'), ('floor_day', 'f'), ('p_conf', 'c'),
1740
  ('alpha2', 'a2'), ('emergency_day', 'e'), ('restarts', 'r'),
1741
  ('iters', 'i'), ('ec_margin', 'ec'), ('ec_pmax', 'pm'),
1742
- ('split_days', 'sd'), ('park_mobility', 'pk')]:
 
1743
  if key in df.columns and df[key].nunique(dropna=False) > 1:
1744
  df['name'] += '/' + tag + df[key].fillna(0).astype(str)
1745
  mean = df.groupby('name')['total_cost'].agg(['mean', 'std']).sort_values('mean')
1746
  print(f'\n=== {label} ===')
1747
  print(mean.round(1).to_string())
 
 
 
 
 
 
 
 
 
 
 
1748
  return mean
1749
 
1750
 
@@ -1771,7 +1820,7 @@ def selection_stability(df, n_random=2000, n_blocks=6, seed=0):
1771
  for key, tag in [('blend', 'b'), ('p_be', 'be'), ('floor_day', 'f'), ('p_conf', 'c'),
1772
  ('alpha2', 'a2'), ('emergency_day', 'e'), ('restarts', 'r'),
1773
  ('iters', 'i'), ('ec_margin', 'ec'), ('ec_pmax', 'pm'),
1774
- ('split_days', 'sd'), ('park_mobility', 'pk')]:
1775
  if key in df.columns and df[key].nunique(dropna=False) > 1:
1776
  df['name'] += '/' + tag + df[key].fillna(0).astype(str)
1777
  pv = df.pivot_table(index='origin', columns='name', values='total_cost')
@@ -1782,24 +1831,35 @@ def selection_stability(df, n_random=2000, n_blocks=6, seed=0):
1782
  rng = np.random.RandomState(seed)
1783
 
1784
  def run(splits, label):
 
 
 
 
 
1785
  sel = {m: 0 for m in names}
1786
  best = {m: 0 for m in names}
1787
- regrets = []
1788
  for A in splits:
1789
  B = np.setdiff1d(np.arange(n), A)
1790
- i = X[A].mean(0).argmin()
1791
- j = X[B].mean(0).argmin()
 
 
 
 
1792
  sel[names[i]] += 1
1793
  best[names[j]] += 1
1794
- regrets.append(X[B].mean(0)[i] - X[B].mean(0)[j])
1795
- k = len(splits)
1796
- print(f'\n--- selection stability: {label} ({k} splits) ---')
 
1797
  print(f"{'config':40s} {'selected%':>9s} {'best-holdout%':>13s} {'mean':>8s}")
1798
  for m in sorted(names, key=lambda m: -sel[m]):
1799
  print(f'{m:40s} {100*sel[m]/k:9.1f} {100*best[m]/k:13.1f} '
1800
  f'{X[:, names.index(m)].mean():8.1f}')
1801
- print(f'mean selection regret {np.mean(regrets):.1f} '
1802
- f'(90th pct {np.percentile(regrets, 90):.1f})')
 
1803
 
1804
  half = n // 2
1805
  run([rng.choice(n, half, replace=False) for _ in range(n_random)],
 
1382
  pay early-replacement for weeks [floor_day 0 = off]
1383
  """
1384
  psum = pred['p42_sum'].iloc[0] if 'p42_sum' in pred else pred['p42'].sum()
1385
+ if plan_tsroute.floor_scale > 0:
1386
+ # cold-start inclusion floor: sum(p42) tracks prevalence only once the
1387
+ # training era has seen it - at warm-up origins it undercounts ~4x and
1388
+ # the count rule parks visibly-dying batteries (46% of all-48 late
1389
+ # cost). Deaths RECORDED in the last 42d are observable at plan time
1390
+ # (the harness itself drops eol <= origin devices) and estimate the
1391
+ # coming window's prevalence without trusting the probability scale.
1392
+ psum = max(psum, plan_tsroute.floor_scale * plan_tsroute.recent_deaths)
1393
  n = int(round(plan_tsroute.alpha * psum))
1394
  order_col = 'rank_score' if 'rank_score' in pred.columns else 'p42'
1395
  chosen = pred[order_col].nlargest(n).index
 
1458
  plan_tsroute.p_conf = 0.5
1459
  plan_tsroute.ec_margin = 0.0
1460
  plan_tsroute.ec_pmax = 25.0
1461
+ plan_tsroute.floor_scale = 0.0 # 0 = off; k in max(sum_p42, k * recent_deaths)
1462
+ plan_tsroute.recent_deaths = 0.0 # deaths recorded in (origin-42d, origin]; set per origin
1463
 
1464
  def plan_tspig(pred, included, locations, start_time, day_q,
1465
  travel_costs=None, settings=None):
 
1667
 
1668
  def walk_forward(table, scenario_ctx, origins, configs):
1669
  """configs: list of dicts(model, planner, p_star, day_q, name)."""
1670
+ # device-level EOL ledger for the cold-start floor: every death is
1671
+ # observable from the moment it happens (eol <= origin), unlike training
1672
+ # labels, which need the full 42d window closed
1673
+ dev_eol = table[['device_id', 'eol']].dropna().drop_duplicates('device_id')
1674
  fitted = {}
1675
  records = []
1676
  for idx in origins:
1677
  ctx = scenario_ctx[idx]
1678
  origin_time = ctx['start_time']
1679
+ recent_deaths = int(((dev_eol['eol'] > origin_time - pd.Timedelta(days=WINDOW_DAYS))
1680
+ & (dev_eol['eol'] <= origin_time)).sum())
1681
+ # unique dead devices visible to training: every death <= origin lands
1682
+ # as a positive in some closed window (the 4d synthetic grid reaches
1683
+ # back >42d), so this equals the training set's unique positives
1684
+ train_deaths = int((dev_eol['eol'] <= origin_time).sum())
1685
  rows = table[table['origin_idx'] == idx].set_index('device_id')
1686
+ # label-free prevalence signal for the earliest origins, where even
1687
+ # recent_deaths lags (recording delay): fresh devices whose corrected
1688
+ # voltage crossed the 2.45 death threshold within the last 42d
1689
+ zone_young = int(((rows['days_below_245'] >= 0)
1690
+ & (rows['days_below_245'] <= WINDOW_DAYS)
1691
+ & (rows['staleness'] <= 7)).sum()) \
1692
+ if 'days_below_245' in rows.columns else 0
1693
 
1694
  preds = {}
1695
  for model_name in {c['model'] for c in configs}:
 
1722
  PLANNERS['tsroute'].ec_margin = cfg.get('ec_margin', 0.0)
1723
  PLANNERS['tsroute'].ec_pmax = cfg.get('ec_pmax', 25.0)
1724
  PLANNERS['tsroute'].split_days = cfg.get('split_days', 0)
1725
+ PLANNERS['tsroute'].floor_scale = cfg.get('floor', 0.0)
1726
+ # floor_gate: apply the floor only while the training set has
1727
+ # too few observed deaths to calibrate sum(p42) (the cold-start
1728
+ # failure); 0 = ungated. floor_zone: strengthen the prevalence
1729
+ # signal with the label-free death-zone count.
1730
+ gate = cfg.get('floor_gate', 0)
1731
+ prev = max(recent_deaths, zone_young) if cfg.get('floor_zone') \
1732
+ else recent_deaths
1733
+ PLANNERS['tsroute'].recent_deaths = (
1734
+ float(prev) if (gate == 0 or train_deaths < gate) else 0.0)
1735
  global PARK_MOBILITY
1736
  PARK_MOBILITY = cfg.get('park_mobility', 0.0)
1737
  global EMERGENCY_DAY
 
1776
  for key, tag in [('blend', 'b'), ('p_be', 'be'), ('floor_day', 'f'), ('p_conf', 'c'),
1777
  ('alpha2', 'a2'), ('emergency_day', 'e'), ('restarts', 'r'),
1778
  ('iters', 'i'), ('ec_margin', 'ec'), ('ec_pmax', 'pm'),
1779
+ ('split_days', 'sd'), ('park_mobility', 'pk'), ('floor', 'fl'),
1780
+ ('floor_gate', 'fg'), ('floor_zone', 'fz')]:
1781
  if key in df.columns and df[key].nunique(dropna=False) > 1:
1782
  df['name'] += '/' + tag + df[key].fillna(0).astype(str)
1783
  mean = df.groupby('name')['total_cost'].agg(['mean', 'std']).sort_values('mean')
1784
  print(f'\n=== {label} ===')
1785
  print(mean.round(1).to_string())
1786
+ # component decomposition by default (phase 19): late/early/ops drove
1787
+ # every real insight since the leaderboard comparison - stop reburying it
1788
+ comps = [c for c in ('late_swap', 'early_swap') if c in df.columns]
1789
+ ops = [c for c in ('travel', 'overtime', 'daily_limit', 'weekly_limit',
1790
+ 'battery_swap', 'building_change', 'room_change')
1791
+ if c in df.columns]
1792
+ if comps:
1793
+ dec = df.groupby('name')[comps].mean()
1794
+ if ops:
1795
+ dec['ops'] = df.groupby('name')[ops].mean().sum(axis=1)
1796
+ print(dec.round(1).reindex(mean.index).to_string())
1797
  return mean
1798
 
1799
 
 
1820
  for key, tag in [('blend', 'b'), ('p_be', 'be'), ('floor_day', 'f'), ('p_conf', 'c'),
1821
  ('alpha2', 'a2'), ('emergency_day', 'e'), ('restarts', 'r'),
1822
  ('iters', 'i'), ('ec_margin', 'ec'), ('ec_pmax', 'pm'),
1823
+ ('split_days', 'sd'), ('park_mobility', 'pk'), ('floor', 'fl')]:
1824
  if key in df.columns and df[key].nunique(dropna=False) > 1:
1825
  df['name'] += '/' + tag + df[key].fillna(0).astype(str)
1826
  pv = df.pivot_table(index='origin', columns='name', values='total_cost')
 
1831
  rng = np.random.RandomState(seed)
1832
 
1833
  def run(splits, label):
1834
+ # decided-splits-only (phase 19): a challenger byte-identical to the
1835
+ # incumbent on most origins used to lose every all-tie split to
1836
+ # alphabetical argmin order - selection% was mechanically capped
1837
+ # (phase 11 had to argue around this by hand). Ties are now excluded
1838
+ # from the denominator and reported separately.
1839
  sel = {m: 0 for m in names}
1840
  best = {m: 0 for m in names}
1841
+ regrets, tied = [], 0
1842
  for A in splits:
1843
  B = np.setdiff1d(np.arange(n), A)
1844
+ ma, mb = X[A].mean(0), X[B].mean(0)
1845
+ if ma.max() - ma.min() < 1e-9:
1846
+ tied += 1
1847
+ continue
1848
+ i = ma.argmin()
1849
+ j = mb.argmin()
1850
  sel[names[i]] += 1
1851
  best[names[j]] += 1
1852
+ regrets.append(mb[i] - mb[j])
1853
+ k = max(1, len(splits) - tied)
1854
+ print(f'\n--- selection stability: {label} ({len(splits)} splits, '
1855
+ f'{tied} all-tie excluded) ---')
1856
  print(f"{'config':40s} {'selected%':>9s} {'best-holdout%':>13s} {'mean':>8s}")
1857
  for m in sorted(names, key=lambda m: -sel[m]):
1858
  print(f'{m:40s} {100*sel[m]/k:9.1f} {100*best[m]/k:13.1f} '
1859
  f'{X[:, names.index(m)].mean():8.1f}')
1860
+ if regrets:
1861
+ print(f'mean selection regret {np.mean(regrets):.1f} '
1862
+ f'(90th pct {np.percentile(regrets, 90):.1f})')
1863
 
1864
  half = n // 2
1865
  run([rng.choice(n, half, replace=False) for _ in range(n_random)],
batteryswap_example/submit.py CHANGED
@@ -55,6 +55,16 @@ EC_PMAX = float(os.environ.get('BATTERYSWAP_EC_PMAX', '25'))
55
  # bucket (7d) instead of forcing one visit day per building. Panel 1679.2 vs
56
  # 1702.0, selected in 100% of random AND blocked half-splits.
57
  SPLIT_DAYS = int(os.environ.get('BATTERYSWAP_SPLIT_DAYS', '7'))
 
 
 
 
 
 
 
 
 
 
58
 
59
 
60
  def build_split_table(locations, timeseries, eol_times, scen_starts, log=print):
@@ -117,8 +127,16 @@ def adaptive_split_plans(split, split_path, log=print, deadline=None):
117
  # LABEL INTEGRITY: only pairs whose window closed before T
118
  train = pool[pool['origin'] <= T - pd.Timedelta(days=WINDOW_DAYS)]
119
  assert (train['origin'] + pd.Timedelta(days=WINDOW_DAYS) <= T).all()
120
- fitted[T] = SklearnRUL(MODEL_KIND).fit(train)
121
- model = fitted[T]
 
 
 
 
 
 
 
 
122
 
123
  feats = features_at_origin(daily, locs, T)
124
  past = betas[betas['week_end'] <= T]['beta']
@@ -133,6 +151,26 @@ def adaptive_split_plans(split, split_path, log=print, deadline=None):
133
  plan_tsroute.ec_margin = EC_MARGIN
134
  plan_tsroute.ec_pmax = EC_PMAX
135
  plan_tsroute.split_days = SPLIT_DAYS
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
  plan = plan_tsroute(pred, None, locs, T, 0.05,
137
  travel_costs=scenario['travel_costs'],
138
  settings=scenario['settings'])
 
55
  # bucket (7d) instead of forcing one visit day per building. Panel 1679.2 vs
56
  # 1702.0, selected in 100% of random AND blocked half-splits.
57
  SPLIT_DAYS = int(os.environ.get('BATTERYSWAP_SPLIT_DAYS', '7'))
58
+ # Phase 17: cold-start inclusion floor. While the training set has seen fewer
59
+ # than FLOOR_GATE unique deaths, sum(p42) undercounts prevalence ~4x and the
60
+ # count rule parks visibly-dying batteries (warm-up block s_0-s_7: 46% of the
61
+ # all-48 late cost, catch rate 0.00-0.17 at s_0-s_2 with ranking AUC 0.97).
62
+ # Floor the count estimate with max(deaths recorded in the last 42d, fresh
63
+ # devices that crossed 2.45 within the last 42d) x FLOOR_SCALE. Inert once
64
+ # the gate closes (s_8+ locally): the 34-origin panel is byte-identical to
65
+ # the champion. Warm-up CV: 4256 -> 2380 at k=2.5, winning 8/8 origins.
66
+ FLOOR_SCALE = float(os.environ.get('BATTERYSWAP_FLOOR', '2.5'))
67
+ FLOOR_GATE = int(os.environ.get('BATTERYSWAP_FLOOR_GATE', '20'))
68
 
69
 
70
  def build_split_table(locations, timeseries, eol_times, scen_starts, log=print):
 
127
  # LABEL INTEGRITY: only pairs whose window closed before T
128
  train = pool[pool['origin'] <= T - pd.Timedelta(days=WINDOW_DAYS)]
129
  assert (train['origin'] + pd.Timedelta(days=WINDOW_DAYS) <= T).all()
130
+ pos_dev = train.loc[train['dies42'] == 1, 'device_id']
131
+ uniq = int(pos_dev.nunique())
132
+ # foreign-fleet fraction: share of training positives that belong
133
+ # to THIS split's fleet. ~0 at a held-out split's early origins
134
+ # (the pooled train table dominates), -> 1 as its own history
135
+ # accumulates. Locally always 1.0, so the foreign branch below is
136
+ # panel-inert by construction.
137
+ own = float(pos_dev.isin(set(locations['battery'])).mean()) if uniq else 0.0
138
+ fitted[T] = (SklearnRUL(MODEL_KIND).fit(train), uniq, own)
139
+ model, train_uniq, own_frac = fitted[T]
140
 
141
  feats = features_at_origin(daily, locs, T)
142
  past = betas[betas['week_end'] <= T]['beta']
 
151
  plan_tsroute.ec_margin = EC_MARGIN
152
  plan_tsroute.ec_pmax = EC_PMAX
153
  plan_tsroute.split_days = SPLIT_DAYS
154
+ plan_tsroute.floor_scale = FLOOR_SCALE
155
+ plan_tsroute.recent_deaths = 0.0
156
+ if FLOOR_SCALE > 0 and train_uniq < FLOOR_GATE:
157
+ # both signals are observable at T: recorded EOLs (the harness
158
+ # itself drops eol <= T devices) and the split's own voltage zone
159
+ recent = int(((eol_times > T - pd.Timedelta(days=WINDOW_DAYS))
160
+ & (eol_times <= T)).sum())
161
+ zone = int(((feats['days_below_245'] >= 0)
162
+ & (feats['days_below_245'] <= WINDOW_DAYS)
163
+ & (feats['staleness'] <= 7)).sum())
164
+ plan_tsroute.recent_deaths = float(max(recent, zone))
165
+ log(f' {scenario["name"]}: cold-start floor active '
166
+ f'(train deaths {train_uniq}, recent {recent}, zone {zone})')
167
+ # NOTE (phase 20): a foreign-fleet floor branch (fire when training
168
+ # positives are mostly another fleet's, own_frac < 0.5) was built here
169
+ # and REMOVED the same day: the fleet-holdout harness showed the
170
+ # probability scale transfers across device subsets (half-fleet
171
+ # training: sum(p42) 15.8 vs 9.1 true deaths - if anything it
172
+ # OVER-counts), so the floor could only add over-inclusion (+15 in
173
+ # the harness, 0 wins). own_frac is still logged for diagnosis.
174
  plan = plan_tsroute(pred, None, locs, T, 0.05,
175
  travel_costs=scenario['travel_costs'],
176
  settings=scenario['settings'])