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 filesGated 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 +132 -3
- batteryswap_example/models_cv.py +70 -10
- batteryswap_example/submit.py +40 -2
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 |
-
- **
|
| 14 |
-
|
| 15 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 1791 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1792 |
sel[names[i]] += 1
|
| 1793 |
best[names[j]] += 1
|
| 1794 |
-
regrets.append(
|
| 1795 |
-
k = len(splits)
|
| 1796 |
-
print(f'\n--- selection stability: {label} ({
|
|
|
|
| 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 |
-
|
| 1802 |
-
|
|
|
|
| 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 |
-
|
| 121 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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'])
|