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 filesSilent 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 +13 -0
- batteryswap_example/features.py +28 -0
- batteryswap_example/models_cv.py +6 -0
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
|