You inherit a feature-blind baseline: for every object it predicts the training-set class-frequency prior, completely ignoring every light-curve observation and every metadata column it was given. Your goal is to lower the weighted multi-class log-loss of your predicted class probabilities against the true class; a sealed verifier re-trains and re-runs your solver on a HIDDEN batch of objects and scores it (lower is better).
Hard Constraints
- You may only edit code under
/app/methods/main/; you may add sibling.pymodules. - Keep the entrypoint signatures
train(train_records) -> Noneandpredict(test_records) -> list[dict], where the returned list has one entry per OBJECT intest_records, in the same order astest_records(positional, not keyed by any ID field). - The environment provides
numpy+pandas+scikit-learn+scipy+lightgbmand the standard library. Both images carry the same set, byte-identical: the stack you develop against is the stack you are graded on. Other third-party packages -- includingxgboost,catboostand the deep-learning frameworks (torch,tensorflow,jax) -- are NOT installed, and there is no network at grading time to fetch them, so importing one raisesModuleNotFoundErrorinside the grading subprocess and the run scores 0. Build your solution on the libraries listed above. - Your submission is graded under a wall-clock budget. The verifier
re-runs your
train()+predict()on the sealed batch in a subprocess capped at 19200 s, inside a container declared at 8 CPUs / 6144 MiB of memory; the whole verifier stage is capped at 25200 s. The sealed batch is roughly 2.4x the size of the visible training set, so a pipeline that just fits inside your own self-check can still overrun at grading time. Budget for that; a run killed by the time or memory cap produces no predictions and scores 0. Note that the CPU allowance is a share, not a reservation: on a busy host the same pipeline can take noticeably longer than it did in your own self-check. /dev/shmis only 64 MiB (the container default) and cannot be enlarged. Amultiprocessing.shared_memorysegment larger than that is created successfully and only faults on the first write, killing the process with SIGBUS and no traceback. If you parallelise, pass large arrays through/tmp-backed files (or just usen_jobson scikit-learn estimators, which does not rely on/dev/shm).- Your solver must be deterministic given the shipped data -- the
verifier calls
train()thenpredict()fresh in its own process; unseeded randomness makes your local self-check unrepresentative of the graded run (seed every model you fit). - Network access is restricted to the model API by an allowlist enforced
outside your container. Solve the task from the data shipped in
/app/data; sourcing labels from any external source is out of bounds, and the allowlist makes it unreachable rather than merely discouraged. The sealed verifier runs with no network at all. - Your submitted
methods/main/must contain only small.pysource files. The verifier rejects (reward 0) any submission that does not look like code: any non-.pyfile, any symlink, or a total submission size over the guard's byte cap is rejected outright.
What You Have
/app/data/train_metadata.csv: 7,848 labelled astronomical objects (a simulated LSST-precursor survey's spectroscopically-confirmed sample). Columns:object_id,ra,decl(sky position, degrees),ddf_bool(1 if observed in the deep-drilling field, a smaller/higher-cadence survey footprint),hostgal_specz/hostgal_photoz/hostgal_photoz_err(spectroscopic / photometric redshift estimate of the host galaxy, and the photo-z's uncertainty; a spectroscopic redshift is a much more expensive measurement to obtain than a photometric one and is not always available -- when it isn't, this column is coded with a negative sentinel value rather than being left blank),distmod(distance modulus, or a-9sentinel for Galactic objects with no meaningful cosmological distance -- purely Galactic objects also havehostgal_photoz = 0),mwebv(Milky Way dust extinction along the line of sight), andtarget(the true class -- your training label)./app/data/train_lightcurves.csv: the actual brightness measurements for those same 7,848 objects. Columns:object_id,mjd(observation time, Modified Julian Date),passband(an integer 0-5, one of six wavelength filters -- roughly ultraviolet-to-infrared, LSST's u,g,r,i,z,y bands),flux/flux_err(measured brightness and its uncertainty -- a difference-imaging flux, i.e. relative to a reference template, so negative values are normal noise, not an error),detected_bool(1 if this particular observation triggered the survey's detection significance threshold). Each object has on the order of 100-300 observations spread unevenly across the six bands and however long the object was monitored./app/methods/main/solver.py: the baseline solver (predicts the training- set class-frequency prior for every object) -- this directory is what gets graded. Improve it in place or rewrite the algorithm entirely. Matching the baseline earns nothing./app/force_score.py: the exact weighted log-loss formula the verifier uses, including the full class list and per-class weight table (the formula and weights are public; only the true held-out classes are sealed)./app/lightcurve_io.py: a small loader that turns the flat CSVs into therecordsyourtrain()/predict()receive -- read it to see the exact record shape (see "What You Submit" below); you do not need to modify it./app/selfcheck.py: a free, unlimited local dry-run (python /app/selfcheck.py) that fits on a stratified internal split of the visible objects and prints the proxy weighted log-loss. It is a proxy only -- the real held-out set is a different, larger batch of objects, so do not overfit to this split.
What You Submit
Edit /app/methods/main/solver.py, keeping the contract:
def train(train_records: list[dict]) -> None:
# Called once, on the labelled re-training objects. Each element is one
# dict PER OBJECT (not per observation row):
# {"object_id": str, "ra": float, "decl": float, "ddf_bool": int,
# "hostgal_specz": float, "hostgal_photoz": float,
# "hostgal_photoz_err": float, "distmod": float, "mwebv": float,
# "lightcurve": [{"mjd": float, "passband": int, "flux": float,
# "flux_err": float, "detected_bool": int}, ...],
# "target": int} # true class label -- your training target
# Fit whatever state you need and stash it (module globals are fine).
...
def predict(test_records: list[dict]) -> list[dict]:
# test_records: same shape as train_records, WITHOUT "target".
# Return one dict per object, SAME ORDER as test_records (positional).
# Each dict maps {class_label: probability}. See force_score.py for the
# full 15-class list (14 classes seen in training + one "anomaly" class
# that appears ONLY in the held-out set, with zero training examples --
# you must still emit some probability mass for it). Missing classes
# default to probability 0; rows need not already sum to 1 (they are
# renormalized before scoring) but should be a genuine probabilistic
# guess, not a constant/garbage row.
...
There is no submit step and no per-attempt feedback on the real held-out
set -- iterate against selfcheck.py, then leave your best solver.py in
place; it is graded once at the end.
How It Is Judged
After your run, the grader copies your methods/main/ into a sealed
verifier, spawns a fresh process that imports it and calls train() on the
sealed re-training objects, then predict() on a HIDDEN batch of objects you
never saw, and scores the weighted multi-class log-loss of your
predicted probabilities against the true class (lower is better):
- Per-object log-loss is
-log(p_true_class)(probabilities are clipped and renormalized to sum to 1 first). - The mean log-loss is taken within each true class separately (so a rare class with a handful of held-out objects counts equally to a class with thousands).
- The per-class means are combined in a weighted average (most classes
weight 1; five "rare/interesting" classes weight 2 -- see
force_score.pyfor the exact table), normalized by the sum of weights actually present in the held-out slice.
Your score improves monotonically as the weighted log-loss falls, so driving it down is always the goal, and there is real headroom between the shipped baseline and a well-engineered solution. Any crash, wrong output length, wrong output order, or a row with no positive probability mass anywhere scores the whole submission 0.