Thibaut Forest Claude Opus 5 (1M context) commited on
Commit
b636dfd
·
1 Parent(s): c9e7e41

Explore voltage-silent deaths: cohort clock rejected by CV, champion unchanged

Browse files

Silent deaths are fast plungers (same 2.40V crossing, ~3-week collapse), not a
separate mechanism. Candidates tested on the silent subpopulation:
- spatial clustering: AUC ~0.52, dead
- mean temperature: AUC 0.64, too weak
- install-cohort clock: AUC 0.906 in isolation (batch explains ~90% of
lifetime variance) but degrades dev CV in every encoding (2074 -> 2279 raw,
2433 overdue-neutral) due to 14%/54% coverage; kept in features.py as
documented-rejected, excluded from FEATURES.

Champion reproduces exactly after revert: dev 2073.9 (logbag/tsroute/a0.8).

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

EXPERIMENTS.md CHANGED
@@ -84,6 +84,19 @@ ranks, physics projections.
84
  | aggressive challenger α=1.15 | — | 1516 (look 7) | leader-like profile (late 722/early 550); kept as `BATTERYSWAP_ALPHA` knob, not default |
85
  | **adaptive runtime refit** (`submit.py`) | | | refits at every scenario origin on the target split's own self-masked history; honest all-48 train metric 2202; 280 s/split native; deadline guards → static-pickle fallback |
86
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
  ## Trajectory
88
 
89
  | champion | dev | test | combined (34) | all-48 metric |
 
84
  | aggressive challenger α=1.15 | — | 1516 (look 7) | leader-like profile (late 722/early 550); kept as `BATTERYSWAP_ALPHA` knob, not default |
85
  | **adaptive runtime refit** (`submit.py`) | | | refits at every scenario origin on the target split's own self-masked history; honest all-48 train metric 2202; 280 s/split native; deadline guards → static-pickle fallback |
86
 
87
+ ## Phase 6 - voltage-silent deaths (exploration, all rejected by CV)
88
+
89
+ Silent deaths are **fast plungers**, not a separate mechanism: all 16 deaths that
90
+ looked healthy 30d out (v_corr > 2.55) were at ~2.47 by 7d out and ~2.41 the day
91
+ before - the same 2.40 V crossing, compressed into ~3 weeks.
92
+
93
+ | candidate | evidence in isolation | dev CV | verdict |
94
+ |---|---|---|---|
95
+ | room/building failure clustering | AUC 0.52-0.54 (chance) | not attempted | dead on arrival |
96
+ | lifetime mean temperature | AUC 0.64; corr(lifetime, temp) 0.21 | not attempted | too weak alone |
97
+ | install-cohort clock (age - batch median lifetime) | **AUC 0.906** on silent pairs; batch explains ~90% of lifetime variance | 2074 -> 2279 (raw), 2433 (overdue-neutral encoding) | **rejected**: 14%/54% coverage - the no-information majority degrades the fit in every encoding |
98
+ | alpha re-sweep 0.7-1.3 (public-LB-motivated 1.15) | public late/early ratio 8 vs leaders' ~1 | minimum stays at **0.8** (2074; 1.15: 2219, losing 12/16 origins) | 1.15 reverted; late saturates ~1100 for alpha >= 1.15 - remaining misses are not an inclusion problem |
99
+
100
  ## Trajectory
101
 
102
  | champion | dev | test | combined (34) | all-48 metric |
batteryswap_example/features.py CHANGED
@@ -184,6 +184,33 @@ def physics_features(daily: pd.DataFrame, origin: pd.Timestamp,
184
  return out
185
 
186
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187
  def build(dataset_path: Path, out_path: Path) -> pd.DataFrame:
188
  locations, timeseries, eol_times, scenarios = load_dataset(dataset_path / 'train')
189
  daily = daily_voltage(timeseries)
@@ -215,6 +242,7 @@ def build(dataset_path: Path, out_path: Path) -> pd.DataFrame:
215
  dead = eol_times[eol_times.notna() & (eol_times <= origin)]
216
  curve = master_curve(dall[dall['day'] <= origin], dead, beta)
217
  f = f.join(physics_features(dall, origin, beta, curve, match=not is_synth))
 
218
  f['synthetic'] = is_synth
219
  f['origin_idx'] = idx
220
  f['origin'] = origin
 
184
  return out
185
 
186
 
187
+ def cohort_features(devices: pd.DataFrame, eol_times: pd.Series,
188
+ origin: pd.Timestamp) -> pd.DataFrame:
189
+ """
190
+ Install-cohort calibrated clock. Batteries from the same install batch die at
191
+ nearly the same age (batch explains ~90% of lifetime variance), so
192
+ age - median(batch lifetimes observed so far) predicts death even when the
193
+ voltage has not moved yet (AUC 0.91 on voltage-silent pairs).
194
+ Uses only deaths <= origin.
195
+ """
196
+ install = devices.set_index('battery')['start_time'].dt.tz_localize(None)
197
+ dead = eol_times[eol_times.notna() & (eol_times <= origin)]
198
+ life = (dead - install.reindex(dead.index)).dt.days.rename('life')
199
+
200
+ out = pd.DataFrame(index=install.index)
201
+ age = (origin - install).dt.days
202
+ for col, key in [('cohort_rel_age', install),
203
+ ('cohort_rel_age_m', install.dt.to_period('M'))]:
204
+ batch_life = life.groupby(key.reindex(life.index)).agg(['median', 'size'])
205
+ med = key.map(batch_life['median'].where(batch_life['size'] >= 2))
206
+ out[col] = age - med
207
+ # neutral-when-unknown encoding: fires only past the batch's observed
208
+ # lifetime, exactly 0 otherwise - no imputation noise for the majority
209
+ out[col.replace('rel_age', 'overdue')] = (age - med).clip(lower=0).fillna(0)
210
+ out.index.name = 'device_id'
211
+ return out
212
+
213
+
214
  def build(dataset_path: Path, out_path: Path) -> pd.DataFrame:
215
  locations, timeseries, eol_times, scenarios = load_dataset(dataset_path / 'train')
216
  daily = daily_voltage(timeseries)
 
242
  dead = eol_times[eol_times.notna() & (eol_times <= origin)]
243
  curve = master_curve(dall[dall['day'] <= origin], dead, beta)
244
  f = f.join(physics_features(dall, origin, beta, curve, match=not is_synth))
245
+ f = f.join(cohort_features(locations, eol_times, origin))
246
  f['synthetic'] = is_synth
247
  f['origin_idx'] = idx
248
  f['origin'] = origin
batteryswap_example/models_cv.py CHANGED
@@ -47,6 +47,12 @@ FEATURES = ['v_last', 'v_drop', 'slope_30', 'slope_90', 'v_min_30',
47
  'staleness', 'age_days',
48
  'slope_lastk', 'drop_recent', 'below_frac', 'accel', 'n_30',
49
  'tau_projected']
 
 
 
 
 
 
50
  # tau_projected = C^-1(v_corr_last) - staleness: targets devices that go dark and
51
  # keep discharging (the model's main blind spot - 25 of 60 test-block misses).
52
  # Other physics features (tau_hat, fit_err, v_corr_last, thresh_dist) remain
 
47
  'staleness', 'age_days',
48
  'slope_lastk', 'drop_recent', 'below_frac', 'accel', 'n_30',
49
  'tau_projected']
50
+ # Rejected by CV despite AUC 0.91 on voltage-silent pairs: cohort_rel_age /
51
+ # cohort_overdue (install-batch calibrated clock, computed in features.py).
52
+ # Coverage is too low at dev-era origins (14% exact / 54% monthly), so the
53
+ # no-information majority degrades the fit in every encoding tried
54
+ # (dev 2074 -> 2279 raw, -> 2433 overdue-neutral). Revisit only if far more
55
+ # batch deaths become observable.
56
  # tau_projected = C^-1(v_corr_last) - staleness: targets devices that go dark and
57
  # keep discharging (the model's main blind spot - 25 of 60 test-block misses).
58
  # Other physics features (tau_hat, fit_err, v_corr_last, thresh_dist) remain