Count every event in a time-series aggregation - #170
Conversation
`compute_agg` collapses events to one row per `(subject_id, time)` before
applying the rolling window. The collapse is index-based: it hands
`aggregate_matrix` the first and last row index of each group, and
`aggregate_matrix` slices `matrix[min_index:max_index]` and skips a window whose
bounds are equal.
`max_index` is the *inclusive* index of the last event in the group, so the
exclusive slice dropped the last event of every timestamp, and dropped a
timestamp holding a single event entirely.
`get_rolling_window_indicies` already adds the 1 for this reason; now so does
the group collapse.
The loss was invisible: the matrix has the right shape, the run exits 0, and the
only symptom is that features are smaller than the data. On a dataset where
every event has its own timestamp it is total -- every time-series aggregation
comes out empty.
Measured on the MEDS-DEV MIMIC-IV demo extract (100 subjects, 1,022,455
timestamped events falling into 96,849 `(subject_id, time)` groups, so 9.47% of
events were being dropped), tabularized at
aggs=[static/present,code/count,value/count,value/sum,value/sum_sqd,value/min,value/max]
and window_sizes=[2h,12h,1d,7d,30d,365d,full]:
aggregation matrices before nnz after nnz delta ratio
static/present 3 74 74 +0 1.000
code/count 21 222,302 237,251 +14,949 1.067
value/count 21 90,104 96,574 +6,470 1.072
value/sum 21 81,827 88,204 +6,377 1.078
value/sum_sqd 21 81,840 88,217 +6,377 1.078
value/min 21 5 4 -1 0.800
value/max 21 429 452 +23 1.054
TOTAL 476,581 510,776 +34,195 1.072
Total counted-event mass rises 10.63% for code/count, which is what the drop
rate predicts: recovering 9.47% of events raises a base of 90.53% by
1/0.9053 - 1 = 10.46%. `static/present` is unchanged, as it is not windowed.
`value/min` loses one stored entry because including a previously dropped event
changed a minimum to zero, which is then no longer stored.
The existing tabularization tests assert matrix shapes and feature names but
never matrix contents, which is why this survived; the new tests assert
contents. All 81 existing tests still pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KLTpjYGQetd7WLrKiXzZk4
`code-quality` failed on the new test for two reasons, both mine: * ruff's DTZ001 rejects `datetime.datetime()` without a `tzinfo`, four times. MEDS event times are timezone-naive, so adding a tzinfo would be wrong; the existing tests never call `datetime()` at all, they parse strings with `pl.col(...).str.to_datetime(...)`. This test now does the same, which removes the lint and matches the repo's own idiom. * ruff-format reflowed two calls, because this repo's line length is 110 and I had wrapped at 100. Verified with the repo's pinned ruff 0.15.10: `ruff check` and `ruff format --check` both clean. The 7 tests still pass and still fail on the parent commit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KLTpjYGQetd7WLrKiXzZk4
|
Follow-up measurement, because "9.5% of events" understates this: the loss is systematic, not On the MIMIC-IV demo extract, per code, over the 1,474 codes with at least 50 events:
Per-code drop rate: p10 = 0.8%, median 4.4%, p90 = 42.1%, max 100%. The worst-hit codes with
So this is not 9.5% of noise spread evenly. It is Does it change model performance?It depends entirely on where the predictive code sits in its group — which is the point. A controlled
The first row is a perfectly learnable signal reduced to chance. The second is unaffected — and note |
Stacked on #169 (
agent/fix-value-feature-filter), which is not yet merged, so its commit appears inthis diff too. The change here is one line plus tests.
The defect
compute_aggcollapses events to one row per(subject_id, time)before applying the rolling window.The collapse is index-based: it hands
aggregate_matrixthe first and last row index of each group,and
aggregate_matrixslicesmatrix[min_index:max_index]and skips a window whose bounds are equal.max_indexis the inclusive index of the last event in the group, so the exclusive slice droppedthe last event of every timestamp, and dropped a timestamp holding a single event entirely.
get_rolling_window_indiciesalready adds the 1 for exactly this reason.The loss is invisible in normal use: the matrix has the right shape, the run exits 0, and the only
symptom is that features are smaller than the data. On a dataset where every event has its own
timestamp it is total — every time-series aggregation comes out empty.
Minimal reproduction,
code/countover columnsA, B, Cwith a label after the last event:A@t1, B@t2, C@t3[0, 0, 0][1, 1, 1]A@t1, B@t1, C@t2[1, 0, 0][1, 1, 1]A@t1, B@t1, C@t1[1, 1, 0][1, 1, 1]Measured effect on real data
MEDS-DEV MIMIC-IV demo extract, 100 subjects, tabularized at
aggs=[static/present,code/count,value/count,value/sum,value/sum_sqd,value/min,value/max],window_sizes=[2h,12h,1d,7d,30d,365d,full],min_code_inclusion_count=10, labels frommortality/in_icu/first_24h. Both arms share one environment, one dataset and one code-metadata file;only this line differs.
The dataset holds 1,022,455 timestamped events in 96,849
(subject_id, time)groups, so 9.47% ofevents were being dropped.
static/presentcode/countvalue/countvalue/sumvalue/sum_sqdvalue/minvalue/maxTotal counted-event mass rises 10.63% for
code/count(1,353,446 → 1,497,309), which is what thedrop rate predicts: recovering 9.47% of events raises a base of 90.53% by
1/0.9053 − 1 = 10.46%. Twoindependent measurements agreeing to a fraction of a percent.
static/presentis unchanged, as it is not windowed.value/minloses one stored entry, becauseincluding a previously dropped event changed a minimum to zero, which is then not stored — the change
is not "every number goes up", it is "every event is seen".
Why the test suite did not catch it
tests/test_tabularize.pyasserts matrix shapes and feature names, and that matrices arenon-empty; it never asserts matrix contents for a time-series aggregation. A matrix that is the
right shape and missing 9.5% of its mass passes every existing check. The added tests assert contents.
Tests
tests/test_generate_summarized_reps.py, 7 tests:code/countsees every event under three timestamplayouts,
value/sumequals the sum of the numeric values under the same three, and a lone event is notdropped. All 7 fail on the parent commit and pass here.
The existing suite is unaffected: 81 passed before, 88 passed after (81 + 7 new), on
Python 3.12.13, linux/aarch64.