Commit ·
2106fa0
1
Parent(s): 7752099
Submission 004: generalization-fixed v3 planner
Browse files
scripts/train_submission.py
CHANGED
|
@@ -32,6 +32,11 @@ def parse_args() -> argparse.Namespace:
|
|
| 32 |
parser.add_argument("--quantile", type=float, default=0.05)
|
| 33 |
parser.add_argument("--event-risk-threshold", type=float, default=0.50)
|
| 34 |
parser.add_argument("--prediction-offset-days", type=float, default=-5.0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
parser.add_argument("--calibration-folds", type=int, default=5)
|
| 36 |
parser.add_argument("--skip-evaluation", action="store_true")
|
| 37 |
return parser.parse_args()
|
|
@@ -65,6 +70,11 @@ def main() -> None:
|
|
| 65 |
event_risk_threshold=args.event_risk_threshold,
|
| 66 |
prediction_offset_days=args.prediction_offset_days,
|
| 67 |
use_expected_cost=True,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
capacity_lookahead_days=21,
|
| 69 |
),
|
| 70 |
)
|
|
@@ -77,12 +87,19 @@ def main() -> None:
|
|
| 77 |
"quantile": args.quantile,
|
| 78 |
"event_risk_threshold": args.event_risk_threshold,
|
| 79 |
"prediction_offset_days": args.prediction_offset_days,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
"calibration_folds": args.calibration_folds,
|
| 81 |
"planner_policy": {
|
| 82 |
"expected_cost": True,
|
| 83 |
"risk_calibration_scale": planner.policy.risk_calibration_scale,
|
| 84 |
"expected_service_cost_hours": planner.policy.expected_service_cost_hours,
|
| 85 |
"expected_gain_margin": planner.policy.expected_gain_margin,
|
|
|
|
|
|
|
| 86 |
"prediction_offset_days": planner.policy.prediction_offset_days,
|
| 87 |
"stale_risk_cutoff_days": planner.policy.stale_risk_cutoff_days,
|
| 88 |
"recent_gap_risk_factor": planner.policy.recent_gap_risk_factor,
|
|
|
|
| 32 |
parser.add_argument("--quantile", type=float, default=0.05)
|
| 33 |
parser.add_argument("--event-risk-threshold", type=float, default=0.50)
|
| 34 |
parser.add_argument("--prediction-offset-days", type=float, default=-5.0)
|
| 35 |
+
parser.add_argument("--risk-calibration-scale", type=float, default=1.5)
|
| 36 |
+
parser.add_argument("--expected-gain-margin", type=float, default=10.0)
|
| 37 |
+
parser.add_argument("--expected-service-cost-hours", type=float, default=2.0)
|
| 38 |
+
parser.add_argument("--minimum-scheduled-batteries", type=int, default=16)
|
| 39 |
+
parser.add_argument("--maximum-scheduled-batteries", type=int, default=20)
|
| 40 |
parser.add_argument("--calibration-folds", type=int, default=5)
|
| 41 |
parser.add_argument("--skip-evaluation", action="store_true")
|
| 42 |
return parser.parse_args()
|
|
|
|
| 70 |
event_risk_threshold=args.event_risk_threshold,
|
| 71 |
prediction_offset_days=args.prediction_offset_days,
|
| 72 |
use_expected_cost=True,
|
| 73 |
+
risk_calibration_scale=args.risk_calibration_scale,
|
| 74 |
+
expected_gain_margin=args.expected_gain_margin,
|
| 75 |
+
expected_service_cost_hours=args.expected_service_cost_hours,
|
| 76 |
+
minimum_scheduled_batteries=args.minimum_scheduled_batteries,
|
| 77 |
+
maximum_scheduled_batteries=args.maximum_scheduled_batteries,
|
| 78 |
capacity_lookahead_days=21,
|
| 79 |
),
|
| 80 |
)
|
|
|
|
| 87 |
"quantile": args.quantile,
|
| 88 |
"event_risk_threshold": args.event_risk_threshold,
|
| 89 |
"prediction_offset_days": args.prediction_offset_days,
|
| 90 |
+
"risk_calibration_scale": args.risk_calibration_scale,
|
| 91 |
+
"expected_gain_margin": args.expected_gain_margin,
|
| 92 |
+
"expected_service_cost_hours": args.expected_service_cost_hours,
|
| 93 |
+
"minimum_scheduled_batteries": args.minimum_scheduled_batteries,
|
| 94 |
+
"maximum_scheduled_batteries": args.maximum_scheduled_batteries,
|
| 95 |
"calibration_folds": args.calibration_folds,
|
| 96 |
"planner_policy": {
|
| 97 |
"expected_cost": True,
|
| 98 |
"risk_calibration_scale": planner.policy.risk_calibration_scale,
|
| 99 |
"expected_service_cost_hours": planner.policy.expected_service_cost_hours,
|
| 100 |
"expected_gain_margin": planner.policy.expected_gain_margin,
|
| 101 |
+
"minimum_scheduled_batteries": planner.policy.minimum_scheduled_batteries,
|
| 102 |
+
"maximum_scheduled_batteries": planner.policy.maximum_scheduled_batteries,
|
| 103 |
"prediction_offset_days": planner.policy.prediction_offset_days,
|
| 104 |
"stale_risk_cutoff_days": planner.policy.stale_risk_cutoff_days,
|
| 105 |
"recent_gap_risk_factor": planner.policy.recent_gap_risk_factor,
|
scripts/validate_submission.py
CHANGED
|
@@ -25,16 +25,47 @@ def _numbers(value: str) -> list[float]:
|
|
| 25 |
return [float(item.strip()) for item in value.split(",") if item.strip()]
|
| 26 |
|
| 27 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
def parse_args() -> argparse.Namespace:
|
| 29 |
parser = argparse.ArgumentParser()
|
| 30 |
parser.add_argument("--dataset-path", type=Path, default=Path("data/raw/train"))
|
| 31 |
parser.add_argument("--folds", type=int, default=5)
|
| 32 |
parser.add_argument("--quantile", type=float, default=0.05)
|
| 33 |
parser.add_argument("--inner-calibration-folds", type=int, default=3)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
parser.add_argument("--risk-scales", default="1.50")
|
| 35 |
parser.add_argument("--gain-margins", default="10")
|
| 36 |
parser.add_argument("--service-costs", default="2")
|
| 37 |
parser.add_argument("--offsets", default="-5")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
parser.add_argument(
|
| 39 |
"--output", type=Path, default=Path("artifacts/submission_cv_final.json")
|
| 40 |
)
|
|
@@ -63,21 +94,30 @@ def main() -> None:
|
|
| 63 |
|
| 64 |
oof_risk = np.full(len(training), np.nan)
|
| 65 |
oof_rul = np.full(len(training), np.nan)
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
)
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
|
| 82 |
due = (
|
| 83 |
training["event_observed"].astype(bool)
|
|
@@ -114,76 +154,84 @@ def main() -> None:
|
|
| 114 |
classification["due_within_horizon_rul_mae_days"] = float(np.mean(due_rul_error))
|
| 115 |
|
| 116 |
policy_results = []
|
| 117 |
-
placeholder_model =
|
| 118 |
for risk_scale in _numbers(args.risk_scales):
|
| 119 |
for gain_margin in _numbers(args.gain_margins):
|
| 120 |
for service_cost in _numbers(args.service_costs):
|
| 121 |
for offset in _numbers(args.offsets):
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
snapshot = training.loc[mask]
|
| 139 |
-
risk = oof_risk[mask]
|
| 140 |
-
rul = oof_rul[mask]
|
| 141 |
-
plan = planner.plan_snapshot(
|
| 142 |
-
snapshot,
|
| 143 |
-
locs,
|
| 144 |
-
scenario["travel_costs"],
|
| 145 |
-
scenario["settings"],
|
| 146 |
-
scenario["start_time"],
|
| 147 |
-
predicted_rul=rul,
|
| 148 |
-
predicted_risk=risk,
|
| 149 |
-
)
|
| 150 |
-
start = pd.Timestamp(scenario["start_time"])
|
| 151 |
-
horizon_end = start + pd.Timedelta(
|
| 152 |
-
days=scenario["settings"].planning_window_days
|
| 153 |
)
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
)
|
| 164 |
-
scores.append(score)
|
| 165 |
-
mean_score = pd.concat(scores, axis=1).mean(axis=1)
|
| 166 |
-
policy_results.append(
|
| 167 |
-
{
|
| 168 |
-
"risk_calibration_scale": risk_scale,
|
| 169 |
-
"expected_gain_margin": gain_margin,
|
| 170 |
-
"expected_service_cost_hours": service_cost,
|
| 171 |
-
"prediction_offset_days": offset,
|
| 172 |
-
"stale_risk_cutoff_days": planner.policy.stale_risk_cutoff_days,
|
| 173 |
-
"recent_gap_risk_factor": planner.policy.recent_gap_risk_factor,
|
| 174 |
-
"stale_risk_factor": planner.policy.stale_risk_factor,
|
| 175 |
-
"building_batch_window_days": planner.policy.building_batch_window_days,
|
| 176 |
-
"capacity_operational_cost_weight": planner.policy.capacity_operational_cost_weight,
|
| 177 |
-
"mean_scheduled_batteries": float(np.mean(scheduled_counts)),
|
| 178 |
-
**{key: float(value) for key, value in mean_score.items()},
|
| 179 |
-
}
|
| 180 |
-
)
|
| 181 |
|
| 182 |
policy_results.sort(key=lambda item: item["total_cost"])
|
| 183 |
report = {
|
| 184 |
"dataset_revision": DATASET_REVISION,
|
| 185 |
"folds": args.folds,
|
| 186 |
"inner_calibration_folds": args.inner_calibration_folds,
|
|
|
|
| 187 |
"quantile": args.quantile,
|
| 188 |
"classification": classification,
|
| 189 |
"policies": policy_results,
|
|
|
|
| 25 |
return [float(item.strip()) for item in value.split(",") if item.strip()]
|
| 26 |
|
| 27 |
|
| 28 |
+
def _integers(value: str) -> list[int]:
|
| 29 |
+
return [int(item.strip()) for item in value.split(",") if item.strip()]
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def _schedule_bands(quotas: str, bands: str) -> list[tuple[int, int]]:
|
| 33 |
+
values = [(quota, quota) for quota in _integers(quotas)]
|
| 34 |
+
for item in bands.split(","):
|
| 35 |
+
item = item.strip()
|
| 36 |
+
if not item:
|
| 37 |
+
continue
|
| 38 |
+
minimum, maximum = item.split(":", maxsplit=1)
|
| 39 |
+
values.append((int(minimum), int(maximum)))
|
| 40 |
+
return list(dict.fromkeys(values))
|
| 41 |
+
|
| 42 |
+
|
| 43 |
def parse_args() -> argparse.Namespace:
|
| 44 |
parser = argparse.ArgumentParser()
|
| 45 |
parser.add_argument("--dataset-path", type=Path, default=Path("data/raw/train"))
|
| 46 |
parser.add_argument("--folds", type=int, default=5)
|
| 47 |
parser.add_argument("--quantile", type=float, default=0.05)
|
| 48 |
parser.add_argument("--inner-calibration-folds", type=int, default=3)
|
| 49 |
+
parser.add_argument(
|
| 50 |
+
"--outer-group",
|
| 51 |
+
choices=("battery", "building"),
|
| 52 |
+
default="building",
|
| 53 |
+
help=(
|
| 54 |
+
"Hold out entire buildings by default to emulate hidden-domain "
|
| 55 |
+
"generalization; use battery only to reproduce legacy reports."
|
| 56 |
+
),
|
| 57 |
+
)
|
| 58 |
parser.add_argument("--risk-scales", default="1.50")
|
| 59 |
parser.add_argument("--gain-margins", default="10")
|
| 60 |
parser.add_argument("--service-costs", default="2")
|
| 61 |
parser.add_argument("--offsets", default="-5")
|
| 62 |
+
parser.add_argument("--schedule-quotas", default="")
|
| 63 |
+
parser.add_argument("--schedule-bands", default="16:20")
|
| 64 |
+
parser.add_argument(
|
| 65 |
+
"--predictions-csv",
|
| 66 |
+
type=Path,
|
| 67 |
+
help="Reuse previously generated OOF predictions for policy-only sweeps.",
|
| 68 |
+
)
|
| 69 |
parser.add_argument(
|
| 70 |
"--output", type=Path, default=Path("artifacts/submission_cv_final.json")
|
| 71 |
)
|
|
|
|
| 94 |
|
| 95 |
oof_risk = np.full(len(training), np.nan)
|
| 96 |
oof_rul = np.full(len(training), np.nan)
|
| 97 |
+
if args.predictions_csv is not None:
|
| 98 |
+
cached = pd.read_csv(args.predictions_csv).set_index(["scenario", "battery"])
|
| 99 |
+
keys = pd.MultiIndex.from_frame(training[["scenario", "battery"]])
|
| 100 |
+
aligned = cached.reindex(keys)
|
| 101 |
+
if aligned[["oof_event_risk", "oof_rul_days"]].isna().any().any():
|
| 102 |
+
raise ValueError("Cached OOF predictions do not cover the validation rows")
|
| 103 |
+
oof_risk[:] = aligned["oof_event_risk"].to_numpy(dtype=float)
|
| 104 |
+
oof_rul[:] = aligned["oof_rul_days"].to_numpy(dtype=float)
|
| 105 |
+
else:
|
| 106 |
+
splitter = GroupKFold(n_splits=args.folds)
|
| 107 |
+
groups = training[args.outer_group].astype(str)
|
| 108 |
+
for fold_number, (train_index, valid_index) in enumerate(
|
| 109 |
+
splitter.split(training, groups=groups), start=1
|
| 110 |
+
):
|
| 111 |
+
model = fit_event_time_model(
|
| 112 |
+
training.iloc[train_index],
|
| 113 |
+
quantile=args.quantile,
|
| 114 |
+
dataset_revision=DATASET_REVISION,
|
| 115 |
+
random_state=2026 + fold_number,
|
| 116 |
+
calibration_folds=args.inner_calibration_folds,
|
| 117 |
+
)
|
| 118 |
+
valid = training.iloc[valid_index]
|
| 119 |
+
oof_risk[valid_index] = model.predict_event_risk(valid)
|
| 120 |
+
oof_rul[valid_index] = model.predict_rul(valid)
|
| 121 |
|
| 122 |
due = (
|
| 123 |
training["event_observed"].astype(bool)
|
|
|
|
| 154 |
classification["due_within_horizon_rul_mae_days"] = float(np.mean(due_rul_error))
|
| 155 |
|
| 156 |
policy_results = []
|
| 157 |
+
placeholder_model = None
|
| 158 |
for risk_scale in _numbers(args.risk_scales):
|
| 159 |
for gain_margin in _numbers(args.gain_margins):
|
| 160 |
for service_cost in _numbers(args.service_costs):
|
| 161 |
for offset in _numbers(args.offsets):
|
| 162 |
+
for minimum, maximum in _schedule_bands(
|
| 163 |
+
args.schedule_quotas, args.schedule_bands
|
| 164 |
+
):
|
| 165 |
+
planner = CompetitionPlanner(
|
| 166 |
+
placeholder_model,
|
| 167 |
+
PlannerPolicy(
|
| 168 |
+
event_risk_threshold=0.50,
|
| 169 |
+
prediction_offset_days=offset,
|
| 170 |
+
use_expected_cost=True,
|
| 171 |
+
risk_calibration_scale=risk_scale,
|
| 172 |
+
expected_service_cost_hours=service_cost,
|
| 173 |
+
expected_gain_margin=gain_margin,
|
| 174 |
+
capacity_lookahead_days=21,
|
| 175 |
+
minimum_scheduled_batteries=minimum,
|
| 176 |
+
maximum_scheduled_batteries=maximum,
|
| 177 |
+
),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 178 |
)
|
| 179 |
+
scores = []
|
| 180 |
+
scheduled_counts = []
|
| 181 |
+
for scenario_name, (scenario, locs, not_dead) in scenario_inputs.items():
|
| 182 |
+
mask = training["scenario"].eq(scenario_name).to_numpy()
|
| 183 |
+
snapshot = training.loc[mask]
|
| 184 |
+
risk = oof_risk[mask]
|
| 185 |
+
rul = oof_rul[mask]
|
| 186 |
+
plan = planner.plan_snapshot(
|
| 187 |
+
snapshot,
|
| 188 |
+
locs,
|
| 189 |
+
scenario["travel_costs"],
|
| 190 |
+
scenario["settings"],
|
| 191 |
+
scenario["start_time"],
|
| 192 |
+
predicted_rul=rul,
|
| 193 |
+
predicted_risk=risk,
|
| 194 |
+
)
|
| 195 |
+
start = pd.Timestamp(scenario["start_time"])
|
| 196 |
+
horizon_end = start + pd.Timedelta(
|
| 197 |
+
days=scenario["settings"].planning_window_days
|
| 198 |
+
)
|
| 199 |
+
scheduled_counts.append(int(plan["day"].le(horizon_end).sum()))
|
| 200 |
+
_, _, score = evaluate_plan(
|
| 201 |
+
plan,
|
| 202 |
+
locs,
|
| 203 |
+
scenario["travel_costs"],
|
| 204 |
+
scenario["settings"],
|
| 205 |
+
eol_times=not_dead,
|
| 206 |
+
start_time=start,
|
| 207 |
+
verbose=0,
|
| 208 |
+
)
|
| 209 |
+
scores.append(score)
|
| 210 |
+
mean_score = pd.concat(scores, axis=1).mean(axis=1)
|
| 211 |
+
policy_results.append(
|
| 212 |
+
{
|
| 213 |
+
"risk_calibration_scale": risk_scale,
|
| 214 |
+
"expected_gain_margin": gain_margin,
|
| 215 |
+
"expected_service_cost_hours": service_cost,
|
| 216 |
+
"prediction_offset_days": offset,
|
| 217 |
+
"minimum_scheduled_batteries": minimum,
|
| 218 |
+
"maximum_scheduled_batteries": maximum,
|
| 219 |
+
"stale_risk_cutoff_days": planner.policy.stale_risk_cutoff_days,
|
| 220 |
+
"recent_gap_risk_factor": planner.policy.recent_gap_risk_factor,
|
| 221 |
+
"stale_risk_factor": planner.policy.stale_risk_factor,
|
| 222 |
+
"building_batch_window_days": planner.policy.building_batch_window_days,
|
| 223 |
+
"capacity_operational_cost_weight": planner.policy.capacity_operational_cost_weight,
|
| 224 |
+
"mean_scheduled_batteries": float(np.mean(scheduled_counts)),
|
| 225 |
+
**{key: float(value) for key, value in mean_score.items()},
|
| 226 |
+
}
|
| 227 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
|
| 229 |
policy_results.sort(key=lambda item: item["total_cost"])
|
| 230 |
report = {
|
| 231 |
"dataset_revision": DATASET_REVISION,
|
| 232 |
"folds": args.folds,
|
| 233 |
"inner_calibration_folds": args.inner_calibration_folds,
|
| 234 |
+
"outer_group": args.outer_group,
|
| 235 |
"quantile": args.quantile,
|
| 236 |
"classification": classification,
|
| 237 |
"policies": policy_results,
|
src/batteryswapai/competition_features.py
CHANGED
|
@@ -20,6 +20,12 @@ NON_FEATURE_COLUMNS = {
|
|
| 20 |
"effective_eol",
|
| 21 |
"target_rul_days",
|
| 22 |
"event_observed",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
}
|
| 24 |
|
| 25 |
|
|
|
|
| 20 |
"effective_eol",
|
| 21 |
"target_rul_days",
|
| 22 |
"event_observed",
|
| 23 |
+
# Dataset/scenario boundary fields are useful to the planner and reports,
|
| 24 |
+
# but they are not battery-health measurements. Learning from them makes
|
| 25 |
+
# same-scenario validation optimistic and breaks on a shifted hidden split.
|
| 26 |
+
"censor_proxy_rul_days",
|
| 27 |
+
"scenario_month_sin",
|
| 28 |
+
"scenario_month_cos",
|
| 29 |
}
|
| 30 |
|
| 31 |
|
src/batteryswapai/competition_planner.py
CHANGED
|
@@ -31,6 +31,8 @@ class PlannerPolicy:
|
|
| 31 |
stale_risk_cutoff_days: float = 7.0
|
| 32 |
recent_gap_risk_factor: float = 0.75
|
| 33 |
stale_risk_factor: float = 0.10
|
|
|
|
|
|
|
| 34 |
|
| 35 |
|
| 36 |
class CompetitionPlanner(Planner):
|
|
@@ -90,16 +92,6 @@ class CompetitionPlanner(Planner):
|
|
| 90 |
work["predicted_risk"] = risks
|
| 91 |
work["target_day"] = np.floor(np.clip(predictions, 0.0, horizon + 1.0)).astype(int)
|
| 92 |
if self.policy.use_expected_cost:
|
| 93 |
-
if "censor_proxy_rul_days" in snapshot.columns:
|
| 94 |
-
proxy_rul = pd.to_numeric(
|
| 95 |
-
snapshot["censor_proxy_rul_days"], errors="coerce"
|
| 96 |
-
).to_numpy(dtype=float)
|
| 97 |
-
else:
|
| 98 |
-
proxy_rul = (
|
| 99 |
-
pd.to_datetime(snapshot["end_time"], errors="coerce").dt.normalize()
|
| 100 |
-
+ pd.to_timedelta(float(settings.unobserved_eol_days), unit="D")
|
| 101 |
-
- start
|
| 102 |
-
).dt.total_seconds().to_numpy() / 86400.0
|
| 103 |
planned_day = np.clip(predictions, 0.0, float(horizon))
|
| 104 |
expected_event_day = np.clip(
|
| 105 |
raw_predictions + self.policy.expected_event_day_shift,
|
|
@@ -123,9 +115,12 @@ class CompetitionPlanner(Planner):
|
|
| 123 |
* np.maximum(emergency_day - expected_event_day, 0.0)
|
| 124 |
+ self.policy.expected_service_cost_hours
|
| 125 |
)
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
|
|
|
|
|
|
|
|
|
| 129 |
)
|
| 130 |
expected_gain = (
|
| 131 |
calibrated_risk * (no_schedule_event_cost - scheduled_event_cost)
|
|
@@ -142,6 +137,26 @@ class CompetitionPlanner(Planner):
|
|
| 142 |
(risks >= self.policy.event_risk_threshold) & (predictions <= horizon)
|
| 143 |
)
|
| 144 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
candidates = work[work["scheduled"]].copy()
|
| 146 |
if not candidates.empty:
|
| 147 |
candidates = self._schedule_candidates(
|
|
|
|
| 31 |
stale_risk_cutoff_days: float = 7.0
|
| 32 |
recent_gap_risk_factor: float = 0.75
|
| 33 |
stale_risk_factor: float = 0.10
|
| 34 |
+
minimum_scheduled_batteries: int = 0
|
| 35 |
+
maximum_scheduled_batteries: int | None = None
|
| 36 |
|
| 37 |
|
| 38 |
class CompetitionPlanner(Planner):
|
|
|
|
| 92 |
work["predicted_risk"] = risks
|
| 93 |
work["target_day"] = np.floor(np.clip(predictions, 0.0, horizon + 1.0)).astype(int)
|
| 94 |
if self.policy.use_expected_cost:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
planned_day = np.clip(predictions, 0.0, float(horizon))
|
| 96 |
expected_event_day = np.clip(
|
| 97 |
raw_predictions + self.policy.expected_event_day_shift,
|
|
|
|
| 115 |
* np.maximum(emergency_day - expected_event_day, 0.0)
|
| 116 |
+ self.policy.expected_service_cost_hours
|
| 117 |
)
|
| 118 |
+
# Conditional on no event within the planning horizon, EOL is
|
| 119 |
+
# later than the horizon. Location end_time is only a dataset
|
| 120 |
+
# boundary and shifted between train and the hidden submission.
|
| 121 |
+
healthy_early_cost = (
|
| 122 |
+
float(settings.early_replacement_penalty_daily)
|
| 123 |
+
* np.maximum(float(horizon) - planned_day, 0.0)
|
| 124 |
)
|
| 125 |
expected_gain = (
|
| 126 |
calibrated_risk * (no_schedule_event_cost - scheduled_event_cost)
|
|
|
|
| 137 |
(risks >= self.policy.event_risk_threshold) & (predictions <= horizon)
|
| 138 |
)
|
| 139 |
|
| 140 |
+
# Absolute probabilities drifted sharply between train and the public
|
| 141 |
+
# split. Keep the workload stable and use the model primarily as a
|
| 142 |
+
# ranking function, which is substantially more robust to calibration
|
| 143 |
+
# shift. Thresholding still decides the set when it lies in the band.
|
| 144 |
+
priority_column = "expected_gain" if self.policy.use_expected_cost else "predicted_risk"
|
| 145 |
+
ranked = work.sort_values(
|
| 146 |
+
[priority_column, "predicted_risk", "predicted_rul", "battery"],
|
| 147 |
+
ascending=[False, False, True, True],
|
| 148 |
+
)
|
| 149 |
+
minimum = min(max(int(self.policy.minimum_scheduled_batteries), 0), len(work))
|
| 150 |
+
maximum = self.policy.maximum_scheduled_batteries
|
| 151 |
+
selected = set(work.index[work["scheduled"]])
|
| 152 |
+
if len(selected) < minimum:
|
| 153 |
+
selected.update(ranked.index[:minimum])
|
| 154 |
+
if maximum is not None and len(selected) > int(maximum):
|
| 155 |
+
selected = set(
|
| 156 |
+
ranked.loc[ranked.index.isin(selected)].index[: int(maximum)]
|
| 157 |
+
)
|
| 158 |
+
work["scheduled"] = work.index.isin(selected)
|
| 159 |
+
|
| 160 |
candidates = work[work["scheduled"]].copy()
|
| 161 |
if not candidates.empty:
|
| 162 |
candidates = self._schedule_candidates(
|
submission_artifacts/planner.joblib
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:88a27b7667a13cf00bf0643bf1cfabb2c186f4456de887228d9bc0e4331459fd
|
| 3 |
+
size 3965521
|
submission_artifacts/planner.json
CHANGED
|
@@ -3,17 +3,21 @@
|
|
| 3 |
"artifact": "/work/submission_artifacts/planner.joblib",
|
| 4 |
"quantile": 0.05,
|
| 5 |
"event_risk_threshold": 0.5,
|
| 6 |
-
"prediction_offset_days": -
|
| 7 |
-
"risk_calibration_scale": 1.
|
| 8 |
-
"expected_gain_margin":
|
| 9 |
"expected_service_cost_hours": 2.0,
|
|
|
|
|
|
|
| 10 |
"calibration_folds": 5,
|
| 11 |
"planner_policy": {
|
| 12 |
"expected_cost": true,
|
| 13 |
-
"risk_calibration_scale": 1.
|
| 14 |
"expected_service_cost_hours": 2.0,
|
| 15 |
-
"expected_gain_margin":
|
| 16 |
-
"
|
|
|
|
|
|
|
| 17 |
"stale_risk_cutoff_days": 7.0,
|
| 18 |
"recent_gap_risk_factor": 0.75,
|
| 19 |
"stale_risk_factor": 0.1,
|
|
@@ -97,16 +101,13 @@
|
|
| 97 |
"voltage_range_day",
|
| 98 |
"location_age_days",
|
| 99 |
"data_gap_days",
|
| 100 |
-
"censor_proxy_rul_days",
|
| 101 |
-
"scenario_month_sin",
|
| 102 |
-
"scenario_month_cos",
|
| 103 |
"voltage_rank_global",
|
| 104 |
"voltage_rank_building",
|
| 105 |
"voltage_rank_room",
|
| 106 |
"building_battery_count",
|
| 107 |
"room_battery_count"
|
| 108 |
],
|
| 109 |
-
"artifact_sha256": "
|
| 110 |
"runtime_versions": {
|
| 111 |
"batteryswap_public": "0.3.4",
|
| 112 |
"fastparquet": "2026.5.0",
|
|
|
|
| 3 |
"artifact": "/work/submission_artifacts/planner.joblib",
|
| 4 |
"quantile": 0.05,
|
| 5 |
"event_risk_threshold": 0.5,
|
| 6 |
+
"prediction_offset_days": -5.0,
|
| 7 |
+
"risk_calibration_scale": 1.5,
|
| 8 |
+
"expected_gain_margin": 10.0,
|
| 9 |
"expected_service_cost_hours": 2.0,
|
| 10 |
+
"minimum_scheduled_batteries": 16,
|
| 11 |
+
"maximum_scheduled_batteries": 20,
|
| 12 |
"calibration_folds": 5,
|
| 13 |
"planner_policy": {
|
| 14 |
"expected_cost": true,
|
| 15 |
+
"risk_calibration_scale": 1.5,
|
| 16 |
"expected_service_cost_hours": 2.0,
|
| 17 |
+
"expected_gain_margin": 10.0,
|
| 18 |
+
"minimum_scheduled_batteries": 16,
|
| 19 |
+
"maximum_scheduled_batteries": 20,
|
| 20 |
+
"prediction_offset_days": -5.0,
|
| 21 |
"stale_risk_cutoff_days": 7.0,
|
| 22 |
"recent_gap_risk_factor": 0.75,
|
| 23 |
"stale_risk_factor": 0.1,
|
|
|
|
| 101 |
"voltage_range_day",
|
| 102 |
"location_age_days",
|
| 103 |
"data_gap_days",
|
|
|
|
|
|
|
|
|
|
| 104 |
"voltage_rank_global",
|
| 105 |
"voltage_rank_building",
|
| 106 |
"voltage_rank_room",
|
| 107 |
"building_battery_count",
|
| 108 |
"room_battery_count"
|
| 109 |
],
|
| 110 |
+
"artifact_sha256": "88a27b7667a13cf00bf0643bf1cfabb2c186f4456de887228d9bc0e4331459fd",
|
| 111 |
"runtime_versions": {
|
| 112 |
"batteryswap_public": "0.3.4",
|
| 113 |
"fastparquet": "2026.5.0",
|