Assign submitted rows to modeling tasks without expanding the grid - #369
Assign submitted rows to modeling tasks without expanding the grid#369annakrystalli wants to merge 7 commits into
Conversation
match_tbl_to_model_task() decided which modeling task each row belonged to by expanding the grid of every value combination the config permits and joining the data to it. It now tests each column against the values that modeling task permits and keeps the rows where every column agrees, so nothing larger than the submission is built and peak memory scales with the file rather than with the config. At G3, 65,000 submitted rows against a config permitting 77.7M combinations, match_tbl_to_model_task() goes from 11,452 MB / 60.1 s to 211 MB / 0.05 s, and check_tbl_value_col() from 13,424 MB / 59.6 s to 222 MB / 0.08 s. Every other check is unchanged. Derived task IDs are no longer matched on. They were before, but only ever NA against NA, since the config gives them no values and callers blanked the column. Rows now come back in submitted order. The config's order was previously a by-product of the grid join that check_tbl_value_col_ascending() depended on, since it reads diff(value) off row order and pmf categories are not necessarily alphabetical. order_by_config = TRUE makes that explicit and the check asks for it. check_tbl_value_col() now takes the character table validate_model_data() already builds, rather than the typed one plus a private character copy of its own. The benchmark follows it, and gains a checks filter so one check can be re-measured without re-running the rest. Breaking: all_character removed from match_tbl_to_model_task(); rows in submitted order by default; check_tbl_value_col() expects character; both error on a missing task ID column. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
helper.R can use the bare call because .lintr excludes it. Files that are not excluded qualify it, as helper-custom-fns.R does. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
micokoch
left a comment
There was a problem hiding this comment.
This looks great. I went through it carefully and was able to follow the narrative and logic. I had a couple of questions/comments, but I asked Claude, who explained them and they made sense. So, there are no special comments from me, but here are Claude's close reading comments:
Gave this a proper read (algorithm, the four breaking changes, both consumer
checks, the grid refactor, and the benchmark write-up) — it's solid, approving.
The core is equivalent to the old grid-join by construction: %in%-per-column
tests the same set membership without materialising the Cartesian product. I
traced the subtle bits and they hold:
- Unused/omitted task IDs become NA, and NA %in% NA is TRUE, so a modeling task
only accepts rows that are NA there — exactly what the grid's NA column did. - Point-estimate output types (output_type_id = NA) match via match(NA, NA).
- Derived task IDs are correctly excluded from both matching and ordering (safe:
a derived value is a function of the matched task IDs, so it can't break a tie
they didn't). - The ascending check stays correct: check_values_ascending() re-groups with
group_by(), which preserves within-group order, so even though the new sort
makes task-ID groups non-contiguous, each group is still output_type_id-
ascending.
The equivalence test against match_via_grid() across all 16 fixtures is exactly
the right way to prove a refactor like this, and lifting the config-reading into
a shared extract_round_property_values() means the grid and set paths can't drift
apart. Perf numbers check out against peak-results.csv, and G3 is now actually
measured (it was defined-but-unmeasured in #360).
One question (non-blocking) on coercion parity, and a few small nits below.
LucieContamin
left a comment
There was a problem hiding this comment.
Thank you for the update, it generally looks great and happy to see that the performance was increased.
I just wanted to check that the new match system keeps only row that are expected from the config files, the output is only used in check to test the variables types and value increase and not the content of the model output (test if its missing some specific value, like a specific target for a location, etc.). Am I correct?
Also, I request changes, because I have some question about some code, but mainly I really struggle with the documentation. It took me a long time to read all of it and try to make sense of some part of it. If it is only me, maybe ignore but I think it will be good to clarify some points, even for the internal documentation.
`%in%` was left to convert the config's values on every comparison, once per column per modeling task, using R's own rendering. The grid path this replaced converted them through hubData::coerce_to_character(), which casts via arrow, and so does the all-character copy of a submission. The two renderings differ: as.character() writes 1e5 as "1e+05" where arrow writes "100000", and TRUE as "TRUE" where arrow writes "true". get_config_mt_value_sets() now casts the values through arrow once, as it extracts them, so the config side renders exactly as it did on the grid path. Matching now compares character against character. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rewrites prompted by review of #369. No behaviour changes. @PARAM output_types said both that NULL means every output type and that callers usually pass one name, without saying that the argument subsets the data as well as matching it. It now says so, and the NULL return for a modeling task offering none of them is left to @returns rather than being described twice. The derived task ID explanation, the order_by_config() header and the output_type_id position loop are split into shorter sentences. "Values that do not sort meaningfully" is replaced by the pmf example that motivates it. Defaults are stated for subset_to_tbl_cols and order_by_config, and all three descriptions of ordering now say that a task ID's positions run through its required values before its optional ones. get_config_mt_value_sets() no longer explains what matching does with its output, since it is not a matching function. extract_round_property_values() says what it does before who shares it. match_cols comes from get_round_task_id_names() rather than the first element of the value sets, which needed an invariant established in another file to read. Two NEWS entries carried implementation mechanics and are cut back to what a caller observes. Two figures in prof-summary.md disagreed with the committed CSV and are corrected to 3.77 s / 0.80 s. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Thanks for the thorough and careful review @LucieContamin ! On your first question: yes, and none of that changes here. Matching returns only rows whose values the config allows and drops the rest, exactly as the grid join did. Its consumers are One thing worth separating out, because it also came up on the On the documentation: that was fair, and it was not only you. I have been through every comment; 6cafdd9 has the rewrites. Thanks for being specific about which passages did not land, it made them easy to find. |
Rewrites prompted by review of #369. No behaviour changes. @PARAM output_types said both that NULL means every output type and that callers usually pass one name, without saying that the argument subsets the data as well as matching it. It now says so, and the NULL return for a modeling task offering none of them is left to @returns rather than being described twice. The derived task ID explanation, the order_by_config() header and the output_type_id position loop are split into shorter sentences. "Values that do not sort meaningfully" is replaced by the pmf example that motivates it. Defaults are stated for subset_to_tbl_cols and order_by_config, and all three descriptions of ordering now say that a task ID's positions run through its required values before its optional ones. get_config_mt_value_sets() no longer explains what matching does with its output, since it is not a matching function. extract_round_property_values() says what it does before who shares it. match_cols comes from get_round_task_id_names() rather than the first element of the value sets, which needed an invariant established in another file to read. Two NEWS entries carried implementation mechanics and are cut back to what a caller observes. Two figures in prof-summary.md disagreed with the committed CSV and are corrected to 3.77 s / 0.80 s. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
check_tbl_value_col_ascending() set derived task ID columns to NA before matching. Matching has not needed that since #355, but the grouping in check_values_ascending() did: it groups by every task ID column, derived ones included, so a file whose derived values are wrong would be split by them and a descending value inside a group would go unreported (#189). check_values_ascending() now leaves derived task IDs out of its grouping columns, the same way assign_mt_row_idx() leaves them out of matching, so one policy is applied one way rather than two. The submission is no longer mutated on the way in, and the error table no longer carries a column of NAs for each derived task ID. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The comment on check_values_ascending() chained three clauses and used "they" and "them" for different referents nine words apart, and cited #189 without saying what it was for. The comment above it justified sorting with a pmf example, but that function only ever sees cdf and quantile output types. #78 is about character cdf thresholds, where "10" sorts before "5". validate_derived_task_ids() ended its abort with "Ignored.", copied from the warning above it where it is true. Nothing is ignored when the call stops. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
4e0e32c to
8c571fc
Compare
LucieContamin
left a comment
There was a problem hiding this comment.
Thank you very much for the update and the additional information. It all looks good to me! I made some minor suggestion, feel free to ignore.
| What to expect of the other rows once #355-#357 land: they should **drop**, and the gaps | ||
| between the G columns should **narrow**. Not to the controls' floor, though. A config | ||
| between the G columns should **narrow**, though not all the way down to the controls. A | ||
| config |
There was a problem hiding this comment.
Very minor note, config is alone on the line.
| touch it again, so what grows is the lookup rather than the work. G1 to G3 multiplies | ||
| the combinations by 6.7 while the longest single value list only goes from 365 to 730. | ||
|
|
||
| **Both now cost almost nothing beyond the setup.** Loading the package and reading |
There was a problem hiding this comment.
Minor suggestion: the "Both" is not clear until we read the second sentence.
| #' itself. `pmf` categories show why that matters: `"low"`, `"moderate"` and | ||
| #' `"high"` have no useful alphabetical order, but the config lists them in the |
There was a problem hiding this comment.
| #' itself. `pmf` categories show why that matters: `"low"`, `"moderate"` and | |
| #' `"high"` have no useful alphabetical order, but the config lists them in the | |
| #' itself. Example of `pmf` categories show why that matters: `"low"`, `"moderate"` and | |
| #' `"high"` have no useful alphabetical order, but the config lists them in the |
Minor suggestion
Resolves #355.
What this changes
match_tbl_to_model_task()finds the modeling task that each submitted row belongs to.Before, it built the grid of every value combination the config allows, then joined the data to that grid. Now it checks each column separately. A row belongs to a modeling task when every one of its values is allowed by that modeling task for that column.
The grid is never built. Memory now depends on the size of the submitted file, not on the size of the config.
Measurements
G1, G2 and G3 all submit about 65,000 rows. They differ only in what the config allows, which gives grids of 11.7M, 38.9M and 77.7M rows. The table shows peak memory and time, before and after.
match_tbl_to_model_taskcheck_tbl_value_colNo other check changed by more than the normal variation between runs. Rounds with more modeling tasks are also faster than before. At size M with 7 modeling tasks, matching goes from 1.07s to 0.71s, and
check_tbl_value_col()from 1.20s to 0.80s.Guide to the files
R/assign_tbl_to_model_task.Ris new and does the work.get_config_mt_value_sets()reads the config once. For each modeling task, it returns the values allowed in each column.assign_mt_rows()selects the rows. It first usesmatch()onoutput_type_idto find the position of each row in the config list for its output type. A row with no position is not accepted by this modeling task. It then checks each task ID column with%in%.One point to note while reading this. The submitted data is never converted.
%in%callsmatch(), andmatch()converts both sides to a common type. That type is character if either side is character. The submitted data is already character, so only the config values are converted. The config holds a few values and the data holds millions of rows, so this is important. This is also whytblmust be character.R/match_tbl_to_model_task.Rhas the same arguments as before and calls the new function.join_tbl_to_model_task()is unchanged.check_tbl_values_required()still uses it, and #357 will replace it in a separate PR.R/expand_model_out_grid.R: the part that reads the config moved toextract_round_property_values(). Bothexpand_model_out_grid()andget_config_mt_value_sets()call it, so they always read the config in the same way. The part that builds the grid is unchanged.R/validate_model_data.Rnow givescheck_tbl_value_col()the character tabletbl_chrinstead of the typed table.Why
check_tbl_value_col()now usestbl_chrIt was the only data check given the typed table, and it converted every column except
valueto character itself. Each run therefore built the character version twice. The exception forvaluewas pointless, because the check convertsvalueto the type the config declares anyway.It now uses the shared character table. This is not a speed change (about 5 ms on 460,000 rows); a run simply holds one character copy instead of two. Output is unchanged for the same input.
Changes in behaviour
These four are listed in NEWS under Breaking Changes.
all_characteris removed. It controlled whether the grid was built as character, so that the grid and the data had the same types and could be joined. There is no join now, andtblis always character, so the argument has nothing left to control.Rows are returned in the order they were submitted. Before, the join returned them in config order.
check_tbl_value_col_ascending()needs that order, because it callsdiff(value)on the rows in the order it receives them. Sorting theoutput_type_idcolumn does not work, becausepmfcategories are not always in alphabetical order (#78). The new argumentorder_by_config = TRUEreturns rows in config order.check_tbl_value_col_ascending()now asks for it, and other checks can use it if they need the same thing.This order is not identical to the old one. Task IDs are now sorted in config order, while
expand.grid()varied the first column fastest. Inside each group of task ID values, the order ofoutput_type_idis the same as before, and that is the only part the ascending check uses.check_tbl_value_col()requires a charactertbl.A missing task ID column is now an error. Before, the grid supplied the missing column, and the join returned a number of rows that did not correspond to the submission. For example, removing
locationfrom a submission of 1,590 rows returned 450 rows and a dplyr warning about a many-to-many join.Derived task IDs are no longer used for matching. They were used before, but only
NAagainstNA: reading the config into value sets sets them toNA, and callers set the column toNAtoo. The result was always true. Skipping them saves one pass over the data per derived task ID.check_tbl_value_col_ascending()still sets them toNA, because it groups by these columns.Other changes in this PR
validate_derived_task_ids()has a newcallargument, so its warning names the function the user called._benchmark/check-calls.Rnow givescheck_tbl_value_colthe character table, likevalidate_model_data(). With the typed table the check still works, butmatch()converts aDatecolumn once per modeling task: 3.81s instead of 0.68s at size M with 7 modeling tasks.HUBVALIDATIONS_BENCHMARK_CHECKSvariable, to run only named checks.Tests
helper-match-tbl.Rkeepsmatch_via_grid(), which is the old implementation. The tests check that the new code puts the same rows in the same modeling tasks, for all 16 hubs that the test suite has a submission for. These cover schema versions v3 to v6, samples, derived task IDs, and rounds with several modeling tasks. Each hub is tested for every output type, and for both values ofsubset_to_tbl_cols.The order is tested separately, using a config where the task IDs vary independently.
The test file runs in 5.4 seconds.
Not in this PR
check_tbl_values()(#356),check_tbl_values_required()(#357) and the sample checks (#368) still build the grid. This is why they are unchanged in the table.submission_tmpl()needs the full grid, because its purpose is to generate every valid combination.