Skip to content

Order same-timestamp events by source-file row, so set_task is the same on every cache build - #1291

Open
solarsys wants to merge 1 commit into
sunlabuiuc:masterfrom
solarsys:fix/deterministic-event-order
Open

solarsys wants to merge 1 commit into
sunlabuiuc:masterfrom
solarsys:fix/deterministic-event-order

Conversation

@solarsys

Copy link
Copy Markdown
Collaborator

Problem

Building the same dataset twice gives different set_task results. Four fresh cache builds of synthetic MIMIC-III, identical in content, gave four different MortalityPredictionMIMIC3 sample sets:

Build Samples Patients
1 2,195 2,115
2 2,194 2,115
3 2,197 2,115
4 2,193 2,114

With a cache held fixed, set_task itself is deterministic: six fresh processes, with 1 and 4 workers, gave byte-identical samples. The variation comes from the cache build.

Cause

  1. _event_transform writes the event table with a parallel sort_values("patient_id"). Each build has exactly the same rows, but the order of a patient's rows differs between builds.
  2. Patient sorts by timestamp alone, with polars' default unstable sort. Events that share a timestamp come out in whatever order the build produced.

Ties are common:

  • Admissions. In synthetic MIMIC-III, 11 patients have two admissions starting at the same time. Their order decides which admission the mortality task drops as "last" and how labels pair up. All 9 patients whose samples changed between the builds above are among these 11.
  • Diagnoses. All diagnoses of an admission carry its discharge time. On a current cache, 269 of 317 admissions checked list their diagnoses in a different order from the source file, so seq_num priority (primary diagnosis first) is lost.
  • Prescriptions. These have dates only, so same-day orders tie. This is the cause of the flaky ATC-3 test noted in Make the ATC-3 drug recommendation test order-insensitive (fixes flaky CI) #1261.

Fix

As proposed in #1261: carry each row's source-file order through the build and use it as the tie-breaker. Breaking ties by column values would be wrong, because it would sort MIMIC diagnoses by code rather than by seq_num.

  • Numbering rows. load_table numbers each table's rows in file order (_source_row), right after reading and before preprocessing and joins. It uses a global cumsum, which is ordered across partitions, the same trick the build already uses for tables without a patient id. If a preprocess_<table> hook drops the column, it is added again.
  • Sorting. Patient sorts by (timestamp, event_type, _source_row). Without the column (a Patient built by hand, or MIMICExtractDataset), it falls back to a stable sort that keeps the input order.
  • Cache key. It includes a new EVENT_FORMAT = 2, so caches written before this change are rebuilt instead of keeping their scrambled order. This costs a one-time rebuild per dataset. I chose it over silently keeping old caches, because a wrong diagnosis order is easy to miss.
  • _source_row has no table/ prefix, so it never appears as an event attribute.

Results

Check Before After
4 fresh builds of synthetic MIMIC-III → set_task 4 different sample sets identical: 2,194 samples, 2,116 patients
Diagnosis order matches DIAGNOSES_ICD.csv (317 admissions) 48 of 317 317 of 317

Tests

tests/core/test_event_tie_order.py, synthetic data only:

  • Tied events fed to Patient in 5 shuffled orders always come out in source order. This fails on master.
  • Without _source_row, the input order is kept.
  • _source_row is not an event attribute.
  • An end-to-end build keeps tied admissions and same-timestamp diagnoses in file order.
  • The event table has _source_row, and changing EVENT_FORMAT changes the cache directory.

examples/event_tie_order.py shows three fresh builds giving the same priority order. docs/api/data/pyhealth.data.Patient.rst has a short "Event order" section, and Patient has a docstring example.

Full core suite: 1,496 tests; the only error is test_sdoh, which also fails on master when the suite is run from tests/ (it looks for its CSV relative to the repo root).

Note

For a given dataset, sample order and some samples change compared with earlier caches, because ties now resolve to source order. The order before was arbitrary and differed per build, so no earlier result depended on it reproducibly.

Closes the underlying issue described in #1261.

🤖 Generated with Claude Code

…me on every cache build

The cached event table is written by a parallel sort on patient_id only, so a
patient's rows came out in a different order on each build, and Patient sorted
by timestamp alone with an unstable sort. Events sharing a timestamp (tied
admissions, all diagnoses of an admission, same-day prescriptions) therefore
changed order between builds: four fresh builds of synthetic MIMIC-III gave
four different MortalityPredictionMIMIC3 sample sets, and diagnoses lost their
seq_num order.

load_table now numbers each table's rows in file order (_source_row), and
Patient sorts by (timestamp, event_type, _source_row), falling back to a stable
sort when the column is absent. EVENT_FORMAT joins the dataset cache key so
older caches are rebuilt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@solarsys
solarsys requested a review from jhnwu3 October 12, 2026 20:28

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant