Phase 41-42: fixed-N ablation rejects the vrel port; public says pooling is a wash
Browse filesPhase 41 discharges phase 39's standing directive ("do not port anything from
that lineage without re-running the ablation on recall at fixed N"). Adds
cv.ablate: holds N fixed and varies only the score, so a feature's effect on
ORDERING is separated from its effect on the topsum count. No planner and no
evaluator - recall at fixed N and the rank distribution of misses are
functions of the score alone.
Deaths caught at fixed N, paired per origin, bootstrap 95% CI:
native N=12 native N=20 static N=12 static N=20
vrel alone -6 [-12,-1] +4 [-1,+9] +6 [+0,+13] -1 [-9,+7]
vmin7_rank alone -1 [ -5,+2] +4 [+0,+9] -5 [-11,+1] +0 [-4,+4]
both -7 [-15,+0] +5 [-1,+11] +4 [ -2,+10] -1 [-9,+7]
v_last alone -39 [-57,-22] -8 [-20,+5] +7 [-10,+25] +5 [-10,+20]
REJECT the port. The sign flips with N on both instruments, every CI straddles
zero, and the ordering is byte-identical at 36-41 of 48 native origins and
228-230 of 240 static ones - these features barely enter the ranking. The one
CI excluding zero is vrel alone, native, N=12, and it is negative. The +138.7
cost penalty measured in phase 40 was therefore almost entirely depth, exactly
as the directive predicted.
The phase-39 claim reproduces and is protocol-specific: v_last alone TIES the
13-feature champion on a held-out fleet (better mean rank, 11.9 vs 13.3) and
is -8.6pp recall@12 natively. Not an over-pruned feature set - the champion's
12 extra features are worth ~8.6pp when refit on the native fleet and ~nothing
to a single static fit on a foreign one.
Phase 42: public scored the phase-40 pooling submission at 1665.89 vs the
1661.45 champion, +4.44. 9 fewer swaps, 363 fewer early-days, 2.4 fewer ops
hours, +51 late-days. A wash with the wrong sign and no recall gain. The
static branch went from 82 training deaths to 246 (164 of them in-fleet and
correctly embargoed) and the ordering did not improve, which retracts the
closing argument phase 41 made for pooling an hour earlier.
Together the two results close the training-data axis: on an unseen fleet the
ranking is essentially determined by raw voltage, and neither richer features
nor 3x more in-fleet data moves it.
Standing champion remains 3c7b883 at 1661.45.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- EXPERIMENTS.md +111 -0
- batteryswap_example/cv.py +147 -1
- batteryswap_example/features.py +21 -5
|
@@ -2024,3 +2024,114 @@ local protocol could ever catch it: the same shape as the hardcoded 42-day park
|
|
| 2024 |
that `repark_for_window` exists to undo. Now a module global that
|
| 2025 |
`plan_tsroute` overwrites from the settings it is handed; verified to
|
| 2026 |
propagate (30 -> 90) and to leave all-48 byte-identical at 1661.7.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2024 |
that `repark_for_window` exists to undo. Now a module global that
|
| 2025 |
`plan_tsroute` overwrites from the settings it is handed; verified to
|
| 2026 |
propagate (30 -> 90) and to leave all-48 byte-identical at 1661.7.
|
| 2027 |
+
|
| 2028 |
+
## Phase 41 - the phase-39 directive discharged: fleet-relative port REJECTED
|
| 2029 |
+
|
| 2030 |
+
Phase 39 left a standing rule: *"Do not port anything from [the blind-harness
|
| 2031 |
+
champion] lineage without re-running the ablation on recall at fixed N."* The
|
| 2032 |
+
`vrel` / `vmin7_rank` port in `features.py` was exactly the case it describes,
|
| 2033 |
+
and it was sitting half-landed - computed by `features.py` and shipped in the
|
| 2034 |
+
packaged parquet, absent from `models_cv.FEATURES`.
|
| 2035 |
+
|
| 2036 |
+
**Why the earlier cost measurement did not discharge the directive.** Under the
|
| 2037 |
+
topsum rule a feature moves `p42`, `p42` moves `sum(p42)`, and `sum(p42)` moves
|
| 2038 |
+
`N`. So a cost delta is the sum of an ordering change and a depth change and
|
| 2039 |
+
cannot separate them - which is how the blind lineage concluded a 4-feature set
|
| 2040 |
+
beat its own champion. `cv.ablate` holds N fixed and varies only the score. No
|
| 2041 |
+
planner and no evaluator are involved: recall at fixed N and the rank
|
| 2042 |
+
distribution of misses are functions of the score alone.
|
| 2043 |
+
|
| 2044 |
+
### Deaths caught at fixed N, paired per origin, bootstrap 95% CI
|
| 2045 |
+
|
| 2046 |
+
| variant | native N=12 | native N=20 | static N=12 | static N=20 |
|
| 2047 |
+
|---|---|---|---|---|
|
| 2048 |
+
| `vrel` alone | **-6 [-12,-1]** | +4 [-1,+9] | +6 [+0,+13] | -1 [-9,+7] |
|
| 2049 |
+
| `vmin7_rank` alone | -1 [-5,+2] | +4 [+0,+9] | -5 [-11,+1] | +0 [-4,+4] |
|
| 2050 |
+
| both | -7 [-15,+0] | +5 [-1,+11] | +4 [-2,+10] | -1 [-9,+7] |
|
| 2051 |
+
| `v_last` alone | **-39 [-57,-22]** | -8 [-20,+5] | +7 [-10,+25] | +5 [-10,+20] |
|
| 2052 |
+
|
| 2053 |
+
454 deaths over 48 native origins; 976 over 240 origin-seeds on the gate.
|
| 2054 |
+
|
| 2055 |
+
**Verdict: reject.** The sign flips with N on both instruments and every CI for
|
| 2056 |
+
the port straddles zero. The single CI that excludes zero is `vrel` alone,
|
| 2057 |
+
native, N=12 - and it is **negative**. Decisive corroboration: the ordering is
|
| 2058 |
+
*byte-identical* at 36-41 of 48 native origins and at 228-230 of 240 static
|
| 2059 |
+
ones. These features barely enter the ranking. The +138.7 cost penalty measured
|
| 2060 |
+
in phase 40 was therefore almost entirely a depth effect, exactly as the
|
| 2061 |
+
directive predicted, and the -24.6 the static gate liked was the same depth
|
| 2062 |
+
effect with the opposite sign.
|
| 2063 |
+
|
| 2064 |
+
### The phase-39 claim reproduces - and it is protocol-specific, not over-pruning
|
| 2065 |
+
|
| 2066 |
+
`v_last` alone was the specific claim: two blind sessions found raw current
|
| 2067 |
+
voltage beating their champion's own ranker at the operating N.
|
| 2068 |
+
|
| 2069 |
+
- **Static gate**: `v_last` alone is statistically **tied** with the full
|
| 2070 |
+
13-feature champion (+7 [-10,+25] at N=12), with a *better* mean rank
|
| 2071 |
+
(11.9 vs 13.3) and a much better missed-death rank (48 vs 65).
|
| 2072 |
+
- **Native**: `v_last` alone is **-39 [-57,-22]** at N=12, rec@12 0.478 vs
|
| 2073 |
+
0.564 - **-8.6pp**. Phase 39 measured +14.3pp on production; same sign, same
|
| 2074 |
+
magnitude class, independently reproduced.
|
| 2075 |
+
|
| 2076 |
+
So the lineage's finding was real *for its protocol* - one static fit on a
|
| 2077 |
+
foreign fleet - and does not survive the refit path. The cause is not an
|
| 2078 |
+
over-pruned feature set and not the threshold rule phase 39 suspected:
|
| 2079 |
+
**the champion's 12 features beyond `v_last` are worth ~8.6pp recall@12 when
|
| 2080 |
+
the model is refit on the native fleet and worth approximately nothing to a
|
| 2081 |
+
single static fit on a foreign one.**
|
| 2082 |
+
|
| 2083 |
+
That is a statement about training data, not about features, and it read at
|
| 2084 |
+
the time as the first independent argument for what phase 40 shipped: in-fleet
|
| 2085 |
+
pooling gives the static branch 164 own deaths against the foreign 82. **The
|
| 2086 |
+
public leaderboard refuted that reading within the hour - see phase 42.**
|
| 2087 |
+
Tripling the static fit's death count bought no recall at all, so training
|
| 2088 |
+
volume is not the binding constraint the argument assumed.
|
| 2089 |
+
|
| 2090 |
+
`features.py` keeps computing both columns (deleting them means regenerating
|
| 2091 |
+
the packaged parquet on submission day for ~1s inside a 360s build budget) but
|
| 2092 |
+
now carries the rejection and these numbers inline, so the port cannot be
|
| 2093 |
+
re-attempted by inspection.
|
| 2094 |
+
|
| 2095 |
+
## Phase 42 - PUBLIC: in-fleet pooling is a wash (1665.89 vs 1661.45)
|
| 2096 |
+
|
| 2097 |
+
Submission d5b03a1 (phase 40, `HELDOUT_POOL=1`). This push also carried phases
|
| 2098 |
+
34-37, which had never reached the remote - but they touch submit.py by one
|
| 2099 |
+
deleted line, so the held-out path's behaviour change is pooling.
|
| 2100 |
+
|
| 2101 |
+
| | pooled (d5b03a1) | champion (3c7b883) | delta |
|
| 2102 |
+
|---|---|---|---|
|
| 2103 |
+
| **total** | **1665.89** | **1661.45** | **+4.44** |
|
| 2104 |
+
| late_swap | 1222.92 | 1212.29 | +10.63 (**+51 late-days**) |
|
| 2105 |
+
| early_swap | 133.69 | 137.47 | -3.78 (-363 early-days) |
|
| 2106 |
+
| ops | 309.28 | 311.69 | -2.41 |
|
| 2107 |
+
| swaps | 571 | 580 | -9 |
|
| 2108 |
+
|
| 2109 |
+
All four inversions land on integers (571 swaps, 5870 late-days, 12834
|
| 2110 |
+
early-days, 54 penalised days), so public is still 48 scenarios at default
|
| 2111 |
+
settings - which also confirms the phase-40 `unobserved_eol_days` fix was inert
|
| 2112 |
+
here, as predicted.
|
| 2113 |
+
|
| 2114 |
+
**Verdict: a wash, marginally negative.** Pooling made the model slightly less
|
| 2115 |
+
eager - 9 fewer swaps, 363 fewer early-days, 2.4 fewer ops hours - and paid 51
|
| 2116 |
+
late-days for it. Net +4.4 on a 1661 baseline is 0.27%, inside any reasonable
|
| 2117 |
+
noise band, but the sign is wrong and there is no recall gain hiding in it.
|
| 2118 |
+
|
| 2119 |
+
**The finding is not "pooling is neutral", it is where the neutrality comes
|
| 2120 |
+
from.** The static branch went from 82 foreign deaths to 246 (82 foreign + 164
|
| 2121 |
+
own, in-fleet, same-fleet, correctly embargoed). That is 3x the training
|
| 2122 |
+
signal, on the target fleet itself, and **the ordering did not improve**. Put
|
| 2123 |
+
beside phase 41 - where `v_last` alone ties the full 13-feature champion on a
|
| 2124 |
+
held-out fleet - the two results say the same thing from opposite directions:
|
| 2125 |
+
|
| 2126 |
+
> On a fleet the model has not seen, the ranking is essentially determined by
|
| 2127 |
+
> raw voltage, and neither richer features nor 3x more in-fleet training data
|
| 2128 |
+
> moves it. The information needed to rank better is not in what we are
|
| 2129 |
+
> feeding the model.
|
| 2130 |
+
|
| 2131 |
+
This closes the training-data axis the way phases 1/6/28 closed the appetite
|
| 2132 |
+
axis. The remaining late cost is the voltage-silent population (recall 0.000 at
|
| 2133 |
+
the operating N, median rank 60 of ~450), and it is invisible to every signal
|
| 2134 |
+
in the current feature space - which is exactly what the phase-38/39 silent-
|
| 2135 |
+
death campaign concluded before it was closed.
|
| 2136 |
+
|
| 2137 |
+
Standing champion remains **3c7b883 at 1661.45**.
|
|
@@ -310,7 +310,148 @@ def _vrel(t):
|
|
| 310 |
if c in t.columns]
|
| 311 |
|
| 312 |
|
| 313 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 314 |
|
| 315 |
|
| 316 |
def main():
|
|
@@ -338,6 +479,11 @@ def main():
|
|
| 338 |
'an appetite/count decision (see module docstring).')
|
| 339 |
elif proto == 'screen':
|
| 340 |
screen()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 341 |
else:
|
| 342 |
raise SystemExit(f'unknown protocol {proto}')
|
| 343 |
print(f'\n[{time.time() - t0:.0f}s]')
|
|
|
|
| 310 |
if c in t.columns]
|
| 311 |
|
| 312 |
|
| 313 |
+
def _vrel_only(t):
|
| 314 |
+
return BASE_FEATURES + ['vrel']
|
| 315 |
+
|
| 316 |
+
|
| 317 |
+
def _vmin7_only(t):
|
| 318 |
+
return BASE_FEATURES + ['vmin7_rank']
|
| 319 |
+
|
| 320 |
+
|
| 321 |
+
def _v_last_only(t):
|
| 322 |
+
# the phase-39 claim under test: two blind sessions found RAW CURRENT
|
| 323 |
+
# VOLTAGE alone beating their champion's ranker at the operating N
|
| 324 |
+
# (.721 vs .668, .703 vs .672). If that reproduces here, our feature set
|
| 325 |
+
# is over-pruned; if it does not, that lineage's ablation was an artifact
|
| 326 |
+
# of its threshold rule and nothing from it should be ported.
|
| 327 |
+
return ['v_last']
|
| 328 |
+
|
| 329 |
+
|
| 330 |
+
VARIANTS = {'base': _base, 'zone': _zone, 'vcorr': _vcorr, 'vrel': _vrel,
|
| 331 |
+
'vrel_only': _vrel_only, 'vmin7_only': _vmin7_only,
|
| 332 |
+
'v_last_only': _v_last_only}
|
| 333 |
+
|
| 334 |
+
ABLATION = ('base', 'vrel_only', 'vmin7_only', 'vrel', 'v_last_only')
|
| 335 |
+
|
| 336 |
+
|
| 337 |
+
# --------------------------------------------------------------------------
|
| 338 |
+
# protocol 4: fixed-N ablation - ORDERING, with the count held out of it
|
| 339 |
+
# --------------------------------------------------------------------------
|
| 340 |
+
|
| 341 |
+
def _order_stats(p42, deaths, ns):
|
| 342 |
+
order = p42.sort_values(ascending=False)
|
| 343 |
+
ranks = {b: r for r, b in enumerate(order.index, start=1)}
|
| 344 |
+
dr = sorted(ranks[b] for b in deaths if b in ranks)
|
| 345 |
+
out = {'n_deaths': len(dr), 'mean_rank': float(np.mean(dr))}
|
| 346 |
+
# COUNTS, not ratios: pooling caught/deaths across origins is the only
|
| 347 |
+
# correct denominator (a 1-death origin must not outvote a 12-death one)
|
| 348 |
+
for n in ns:
|
| 349 |
+
out[f'c{n}'] = sum(1 for r in dr if r <= n)
|
| 350 |
+
tail = [r for r in dr if r > max(ns)]
|
| 351 |
+
out['miss_rank'] = float(np.median(tail)) if tail else np.nan
|
| 352 |
+
return out
|
| 353 |
+
|
| 354 |
+
|
| 355 |
+
def ablate(variants=ABLATION, protocol='all48', origins=range(48),
|
| 356 |
+
ns=(8, 12, 16, 20, 30), seeds=(0, 1, 2, 3, 4),
|
| 357 |
+
dataset_path=Path('dataset')):
|
| 358 |
+
"""Phase-39's standing directive, made runnable.
|
| 359 |
+
|
| 360 |
+
A feature ablation scored on COST cannot answer whether a feature helped:
|
| 361 |
+
under the topsum rule the feature moves p42, p42 moves sum(p42), and
|
| 362 |
+
sum(p42) moves N - so depth and order change together and the cost delta
|
| 363 |
+
is their sum. That is precisely how the blind lineage concluded a
|
| 364 |
+
4-feature set beat its own champion. This protocol holds N FIXED and
|
| 365 |
+
varies only the score, so the only thing being measured is the ordering.
|
| 366 |
+
|
| 367 |
+
No planner and no evaluator are involved: recall at fixed N and the rank
|
| 368 |
+
distribution of missed deaths are functions of the score alone.
|
| 369 |
+
"""
|
| 370 |
+
t = load_table(dataset_path)
|
| 371 |
+
recs = []
|
| 372 |
+
if protocol == 'all48':
|
| 373 |
+
ctx = mc.load_context(dataset_path)
|
| 374 |
+
for idx in origins:
|
| 375 |
+
c = ctx[idx]
|
| 376 |
+
T = c['start_time']
|
| 377 |
+
tr = mc.training_pairs(t, T)
|
| 378 |
+
if tr['dies42'].sum() < 3:
|
| 379 |
+
continue
|
| 380 |
+
rows = (t[t['origin_idx'] == idx].set_index('device_id')
|
| 381 |
+
.reindex(c['locs']['battery']).dropna(subset=['v_last']))
|
| 382 |
+
eol = c['eol'].reindex(rows.index)
|
| 383 |
+
deaths = set(eol.index[(eol > T)
|
| 384 |
+
& (eol <= T + pd.Timedelta(days=WINDOW_DAYS))])
|
| 385 |
+
if not deaths:
|
| 386 |
+
continue
|
| 387 |
+
for v in variants:
|
| 388 |
+
mc.FEATURES = list(VARIANTS[v](t))
|
| 389 |
+
p = mc.SklearnRUL(CHAMPION['model']).fit(tr).predict(rows)['p42']
|
| 390 |
+
recs.append(dict(_order_stats(p, deaths, ns),
|
| 391 |
+
origin=idx, variant=v, seed=0))
|
| 392 |
+
print(f's_{idx}: {len(deaths)} deaths', flush=True, file=sys.stderr)
|
| 393 |
+
else:
|
| 394 |
+
locations, timeseries, eol_times, scenarios = mc.load_dataset(
|
| 395 |
+
dataset_path / 'train')
|
| 396 |
+
dev_b = locations.drop_duplicates('battery').set_index('battery')['building']
|
| 397 |
+
t['bldg'] = t['device_id'].map(dev_b)
|
| 398 |
+
closed = t['origin'] <= t['origin'].max() - pd.Timedelta(days=WINDOW_DAYS)
|
| 399 |
+
allsc = list(mc.iterate_scenarios(locations, timeseries, eol_times,
|
| 400 |
+
scenarios))
|
| 401 |
+
for seed in seeds:
|
| 402 |
+
fleet_a, fleet_b = fleet_halves(locations, seed)
|
| 403 |
+
foreign = t[closed & t['bldg'].isin(fleet_a)]
|
| 404 |
+
models = {}
|
| 405 |
+
for v in variants:
|
| 406 |
+
mc.FEATURES = list(VARIANTS[v](t))
|
| 407 |
+
models[v] = mc.SklearnRUL(CHAMPION['model']).fit(foreign)
|
| 408 |
+
for idx in origins:
|
| 409 |
+
scenario, locs, cut, eol = allsc[idx]
|
| 410 |
+
T = pd.Timestamp(scenario['start_time'])
|
| 411 |
+
locs_b = locs[locs['building'].isin(fleet_b)]
|
| 412 |
+
rows = (t[t['origin_idx'] == idx].set_index('device_id')
|
| 413 |
+
.reindex(locs_b['battery']).dropna(subset=['v_last']))
|
| 414 |
+
e = eol.reindex(rows.index)
|
| 415 |
+
deaths = set(e.index[(e > T)
|
| 416 |
+
& (e <= T + pd.Timedelta(days=WINDOW_DAYS))])
|
| 417 |
+
if not deaths:
|
| 418 |
+
continue
|
| 419 |
+
for v in variants:
|
| 420 |
+
mc.FEATURES = list(VARIANTS[v](t))
|
| 421 |
+
p = models[v].predict(rows)['p42']
|
| 422 |
+
recs.append(dict(_order_stats(p, deaths, ns),
|
| 423 |
+
origin=idx, variant=v, seed=seed))
|
| 424 |
+
print(f'seed {seed} done', flush=True, file=sys.stderr)
|
| 425 |
+
return pd.DataFrame(recs), ns
|
| 426 |
+
|
| 427 |
+
|
| 428 |
+
def ablation_report(df, ns, label, ref='base'):
|
| 429 |
+
print(f'\n=== {label} ===')
|
| 430 |
+
g = df.groupby('variant')
|
| 431 |
+
deaths = g['n_deaths'].sum()
|
| 432 |
+
print(f"{'variant':13s} {'deaths':>6s} " +
|
| 433 |
+
' '.join(f'rec@{n:<3d}' for n in ns) +
|
| 434 |
+
f" {'mean_rk':>7s} {'miss_rk':>7s}")
|
| 435 |
+
for v in df['variant'].unique():
|
| 436 |
+
d = df[df['variant'] == v]
|
| 437 |
+
cells = ' '.join(f'{d[f"c{n}"].sum() / deaths[v]:6.3f}' for n in ns)
|
| 438 |
+
print(f'{v:13s} {int(deaths[v]):6d} {cells} '
|
| 439 |
+
f'{d["mean_rank"].mean():7.1f} {d["miss_rank"].median():7.1f}')
|
| 440 |
+
|
| 441 |
+
# paired against the reference, per (seed, origin): the only comparison
|
| 442 |
+
# that survives the origin-to-origin variance in how many deaths there are
|
| 443 |
+
b = df[df['variant'] == ref].set_index(['seed', 'origin'])
|
| 444 |
+
print(f'\npaired vs {ref} (per origin, deaths caught at fixed N):')
|
| 445 |
+
for v in df['variant'].unique():
|
| 446 |
+
if v == ref:
|
| 447 |
+
continue
|
| 448 |
+
o = df[df['variant'] == v].set_index(['seed', 'origin'])
|
| 449 |
+
cells = []
|
| 450 |
+
for n in ns:
|
| 451 |
+
d = (o[f'c{n}'] - b[f'c{n}']).dropna()
|
| 452 |
+
w, l = int((d > 0).sum()), int((d < 0).sum())
|
| 453 |
+
cells.append(f'{d.sum():+4.0f} ({w}W/{l}L)')
|
| 454 |
+
print(f' {v:13s} ' + ' '.join(cells))
|
| 455 |
|
| 456 |
|
| 457 |
def main():
|
|
|
|
| 479 |
'an appetite/count decision (see module docstring).')
|
| 480 |
elif proto == 'screen':
|
| 481 |
screen()
|
| 482 |
+
elif proto == 'ablate':
|
| 483 |
+
which = sys.argv[2] if len(sys.argv) > 2 else 'all48'
|
| 484 |
+
df, ns = ablate(protocol=which)
|
| 485 |
+
df.to_csv(f'/tmp/cv_ablate_{which}.csv', index=False)
|
| 486 |
+
ablation_report(df, ns, f'fixed-N ablation / {which}')
|
| 487 |
else:
|
| 488 |
raise SystemExit(f'unknown protocol {proto}')
|
| 489 |
print(f'\n[{time.time() - t0:.0f}s]')
|
|
@@ -112,15 +112,31 @@ def features_at_origin(daily: pd.DataFrame, devices: pd.DataFrame,
|
|
| 112 |
|
| 113 |
feats['accel'] = feats['slope_30'] - feats['slope_90']
|
| 114 |
|
| 115 |
-
# ---- fleet-relative encoding -----------------------
|
| 116 |
# Ported from the blind-harness champion, where vrel and vmin7_rank were
|
| 117 |
# the two largest single-feature wins. Both are computed at PLAN time over
|
| 118 |
# the fleet actually in front of the planner, so they absorb a fleet-level
|
| 119 |
# voltage shift that the absolute features (v_last, v_min_30, ...) cannot.
|
| 120 |
-
# The motivation
|
| 121 |
-
#
|
| 122 |
-
#
|
| 123 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
#
|
| 125 |
# Deliberately NOT filled here. The classifier pipeline median-imputes and
|
| 126 |
# then scales, which parks a stale/dark device at "unremarkable" so it is
|
|
|
|
| 112 |
|
| 113 |
feats['accel'] = feats['slope_30'] - feats['slope_90']
|
| 114 |
|
| 115 |
+
# ---- fleet-relative encoding (REJECTED - phase 41) ----------------------
|
| 116 |
# Ported from the blind-harness champion, where vrel and vmin7_rank were
|
| 117 |
# the two largest single-feature wins. Both are computed at PLAN time over
|
| 118 |
# the fleet actually in front of the planner, so they absorb a fleet-level
|
| 119 |
# voltage shift that the absolute features (v_last, v_min_30, ...) cannot.
|
| 120 |
+
# The motivation was TRANSFER to a held-out fleet.
|
| 121 |
+
#
|
| 122 |
+
# NOT IN models_cv.FEATURES, and they must not be added. Re-ablated on
|
| 123 |
+
# recall at FIXED N (the phase-39 directive), which is the only way to see
|
| 124 |
+
# a feature's effect on ordering separately from its effect on the topsum
|
| 125 |
+
# count. Deaths caught, paired per origin, bootstrap 95% CI:
|
| 126 |
+
#
|
| 127 |
+
# native all48 static gate (5 splits)
|
| 128 |
+
# vrel+vmin7 N=12 -7 [-15,+0] N=12 +4 [-2,+10]
|
| 129 |
+
# N=20 +5 [ -1,+11] N=20 -1 [-9, +7]
|
| 130 |
+
#
|
| 131 |
+
# The sign flips with N on BOTH instruments and every CI straddles zero.
|
| 132 |
+
# The ordering is untouched at 36-41 of 48 native origins and 228 of 240
|
| 133 |
+
# static ones - these features barely enter the ranking at all. The one CI
|
| 134 |
+
# that excludes zero is vrel alone, native, N=12: -6 [-12,-1], negative.
|
| 135 |
+
# Cost confirms it: all48 1800.4 vs 1661.7 (+138.7).
|
| 136 |
+
#
|
| 137 |
+
# Left computed rather than deleted only to avoid regenerating the
|
| 138 |
+
# packaged parquet on submission day; the two columns cost ~1s inside
|
| 139 |
+
# build_split_table's 360s budget. Delete them when there is slack.
|
| 140 |
#
|
| 141 |
# Deliberately NOT filled here. The classifier pipeline median-imputes and
|
| 142 |
# then scales, which parks a stale/dark device at "unremarkable" so it is
|