Thibaut Forest
Claude Fable 5
Phase 34-35: retire cross-fleet CV, re-screen 72 configs, champion unchanged
7d93fb8 Download batteryswap_example/aux_features.py from thibautforest/BatterySwapAI2026-Example: direct link, hf CLI and curl.
- Browser
- Download file 4.19 kB
-
https://huggingface.co/thibautforest/BatterySwapAI2026-Example/resolve/main/batteryswap_example/aux_features.py
- Command line
-
hf download hf://thibautforest/BatterySwapAI2026-Example/batteryswap_example/aux_features.py
-
curl -L -o aux_features.py https://huggingface.co/thibautforest/BatterySwapAI2026-Example/resolve/main/batteryswap_example/aux_features.py
4.19 kB
| """ | |
| Auxiliary (device, origin) features from raw hourly readings. | |
| Everything in features.py is built on daily *medians*; this module mines the | |
| within-day and meta-signal structure instead - discharge depth (daily voltage | |
| range/std), telemetry intensity, thermal exposure, and reporting-gap dynamics. | |
| Motivation: 27 of 60 remaining test-block misses had p42 < 0.10 because their | |
| median voltage had not moved by the origin; usage stress may lead the droop. | |
| Cached to dataset/train/aux_features.parquet, keyed (device_id, origin_idx), | |
| aligned with features.parquet's origins. Only data <= origin is used. | |
| """ | |
| from pathlib import Path | |
| import os | |
| import sys | |
| import numpy as np | |
| import pandas as pd | |
| MIN_TEMP, MAX_TEMP = 10, 30 | |
| AUX_FEATURES = ['rng_30', 'rng_90', 'rng_ratio', 'vstd_30', 'hot_90', | |
| 't_med_90', 'reads_30', 'reads_90', 'oob_90', | |
| 'gap_max_60', 'v_min_life'] | |
| def day_stats(timeseries: pd.DataFrame) -> pd.DataFrame: | |
| df = timeseries.reset_index() | |
| df['device_id'] = df['device_id'].astype(str) | |
| df['day'] = df['end_time'].dt.normalize() | |
| inband = df[(df['temperature'] > MIN_TEMP) & (df['temperature'] < MAX_TEMP)] | |
| g = inband.groupby(['device_id', 'day'])['voltage'] | |
| stats = pd.DataFrame({ | |
| 'v_rng': g.max() - g.min(), | |
| 'v_std': g.std(), | |
| 'v_med': g.median(), | |
| }) | |
| ga = df.groupby(['device_id', 'day']) | |
| stats['n_reads'] = ga.size() | |
| stats['t_med'] = ga['temperature'].median() | |
| stats['hot'] = ga['temperature'].apply(lambda t: (t > 28).mean()) | |
| stats['oob'] = ga['temperature'].apply( | |
| lambda t: ((t <= MIN_TEMP) | (t >= MAX_TEMP)).mean()) | |
| return stats.reset_index().sort_values('day') | |
| def aux_at_origin(stats: pd.DataFrame, origin: pd.Timestamp) -> pd.DataFrame: | |
| h = stats[stats['day'] <= origin] | |
| out = {} | |
| for w in (30, 90): | |
| hw = h[h['day'] > origin - pd.Timedelta(days=w)] | |
| gw = hw.groupby('device_id') | |
| if w == 30: | |
| out['rng_30'] = gw['v_rng'].mean() | |
| out['vstd_30'] = gw['v_std'].mean() | |
| out['reads_30'] = gw['n_reads'].mean() | |
| else: | |
| out['rng_90'] = gw['v_rng'].mean() | |
| out['reads_90'] = gw['n_reads'].mean() | |
| out['hot_90'] = gw['hot'].mean() | |
| out['t_med_90'] = gw['t_med'].mean() | |
| out['oob_90'] = gw['oob'].mean() | |
| h60 = h[h['day'] > origin - pd.Timedelta(days=60)] | |
| def max_gap(days): | |
| d = np.sort(days.values) | |
| edges = np.concatenate([d, [np.datetime64(origin)]]) | |
| return np.diff(edges).max() / np.timedelta64(1, 'D') if len(d) else np.nan | |
| out['gap_max_60'] = h60.groupby('device_id')['day'].apply(max_gap) | |
| out['v_min_life'] = h.groupby('device_id')['v_med'].min() | |
| f = pd.DataFrame(out) | |
| f['rng_ratio'] = f['rng_30'] / f['rng_90'].replace(0, np.nan) | |
| return f[AUX_FEATURES] | |
| def build(dataset_path: Path) -> pd.DataFrame: | |
| from batteryswap_public.utils import load_dataset | |
| locations, timeseries, eol_times, scenarios = load_dataset(dataset_path / 'train') | |
| table = pd.read_parquet(dataset_path / 'train' / 'features.parquet') | |
| origins = (table[['origin_idx', 'origin']].drop_duplicates() | |
| .sort_values('origin_idx')) | |
| stats = day_stats(timeseries) | |
| rows = [] | |
| for _, r in origins.iterrows(): | |
| f = aux_at_origin(stats, r['origin']) | |
| f['origin_idx'] = r['origin_idx'] | |
| rows.append(f.reset_index().rename(columns={'index': 'device_id'})) | |
| print(f"aux origin {r['origin_idx']} {r['origin'].date()}: {len(f)}", | |
| file=sys.stderr) | |
| out = pd.concat(rows, ignore_index=True) | |
| out.to_parquet(dataset_path / 'train' / 'aux_features.parquet') | |
| return out | |
| def load(dataset_path: Path = Path('dataset'), force: bool = False) -> pd.DataFrame: | |
| p = dataset_path / 'train' / 'aux_features.parquet' | |
| if p.exists() and not force: | |
| return pd.read_parquet(p) | |
| return build(dataset_path) | |
| if __name__ == '__main__': | |
| dataset_path = Path(os.environ.get('BATTERYSWAP_DATASET_PATH', 'dataset')) | |
| t = load(dataset_path, force='--force' in sys.argv) | |
| print(t.describe().round(3).to_string()) | |