Add trailing_nans parameter to control NaN placement in sort results#235
Add trailing_nans parameter to control NaN placement in sort results#235r-devulap wants to merge 11 commits into
Conversation
All sorting and selection routines (qsort, qselect, partial_qsort,
argsort, argselect) now accept an optional `bool trailing_nans`
parameter (default: true). When hasnan=true:
- trailing_nans=true (default): NaN values appear at the end of the
result, independent of sort direction.
- trailing_nans=false: NaN values appear at the beginning of the
result, independent of sort direction.
Previously, NaN placement was coupled to the sort direction: ascending
always put NaN at the end, descending always put NaN at the beginning.
This change decouples the two.
Implementation notes:
- qsort path: replace_inf_with_nan now takes both `descending` and
`trailing_nans`; when they differ, the real-value portion is rotated
before NaN slots are written back.
- qselect path: NaN is moved to the desired end before partitioning,
keeping the existing move_nans_to_{end,start}_of_array helpers.
- argsort path: after std_argsort_withnan + optional reverse, a
std::rotate moves NaN indices to the desired end if needed.
- argselect path: comparator lambda respects trailing_nans.
- keyvalue_qsort/select path: descending reverse now only reverses
the real portion [0, index_last_elem], leaving trailing NaN in place.
- Scalar fallback: four NaN-aware comparators (compare_nan_end/begin,
compare_arg_nan_end/begin) added to utils/custom-compare.h.
Tests updated to use compare_nan_end for descending reference sorts
and to skip value-comparison checks when arr[k] is NaN.
|
Nice, I have to take a slightly closer look. But should it be |
|
Thanks, this is nice! Going to take a deeper look soon - but, I was wondering if it's actually easier to just change |
It might easier to just reuse existing helper functions: |
- Add bool descending = false parameter to all argselect APIs and internal implementations (avx512, avx2, scalar, static, dispatch) - In xss_argselect: reverse the index array after partitioning when descending is true; pass descending to std_argselect_withnan for NaN-array fast path - std_argselect_withnan: honour descending flag in the comparator - scalar argselect: pick comparator once based on descending and trailing_nans, then call nth_element once - Tests: replace test_argselect with four typed tests covering ascending, descending, trailing NaNs and leading NaNs - IS_ARG_PARTITIONED: add descending parameter forwarded to IS_ARR_PARTITIONED
argselect_ always partitions in ascending order. For descending, we must select at position arrsize-1-k so the k-th largest lands at that mirror index; reversing then moves it to position k with the correct left/right partition invariant.
|
@seberg @MaanasArora This looks good to me - happy to merge once you sign off. |
NumPy, as of today, does not use a descending flag. I’m not sure which other libraries rely on this flag, and I’m not particularly concerned about that change. The newly introduced trailing_nans flag handles NaN placement independently of sort order—it simply lets users specify where NaNs should appear, regardless of whether the sort is ascending or descending. Personally, I don’t find assigning an order to NaNs meaningful. In that sense, this flag mainly provides users with a clear preference for where they want their NaNs. |
|
Sounds good, I prefer the preference to be "at the end" or "at the front", rather than "NaNs are considered larger/smaller". |
|
Thanks a lot, I had a brief look and this makes sense to me! I like this way of moving nans to the start/end and sorting the rest; within the nans we don't have an order so hopefully this approach could even have performance benefits, may be nice to run the benchmarks! Agreed that thinking of NaNs as smaller/larger is confusing in most cases, so position is better. Would |
seberg
left a comment
There was a problem hiding this comment.
Thanks, LGTM. The one ask that I think would be nice is to add all-NaN tests since codex has a point that the logic around it works out but it is a bit fishy.
The compare_arg_* feels like it could be refactored for pretty code, but that doesn't seem too important.
You seem to change the approach for partitioning from NaN replacement to NaN moving. That seems sensible to me, I am just a bit curious what that means speed-wise (not vital either to me, we may also see it in NumPy when bumping)?
There was a problem hiding this comment.
Accidental commit, please remove.
| if (UNLIKELY(hasnan)) { | ||
| if (descending) { | ||
| if (!trailing_nans) { | ||
| index_first_elem = move_nans_to_start_of_array(arr, arrsize); |
There was a problem hiding this comment.
Codex noticed that index_first_elem can overflow to MAX_SIZE_T here and in similar paths. That seems to work out just fine with qselect_ overflowing to consider the range as empty.
(Since this is an index and not a size, -1/MAX_SIZE_T is effectively invalid after all.)
It might be clearer to have inclusive indexes, but honestly, I think it is OK, it would be nice to add an "all_nan" test case.
| else { | ||
| cmp = compare_arg_nan_begin<T, std::less<T>>(arr); | ||
| } | ||
| } |
There was a problem hiding this comment.
Could be nice to put this into a get_argcmp_func to avoid duplicating this if that works.
Might be possible to refactor it more so that the arg comparisons use the get_cmp_func() to avoid the compare_arg_* family, but I don't think it's vital.
Summary of changes in
nanbranchtrailing_nansparameterAdded
bool trailing_nans = trueto all sort/select routines (qsort,qselect,partial_qsort,argsort,argselect). Whenhasnan=true, this controls where NaN values land in the result:trailing_nans=true(default): NaNs at the end, regardless of sort directiontrailing_nans=false: NaNs at the beginning, regardless of sort directionPreviously the library replaced NaNs with
±infas a workaround; now it partitions them directly to the correct end. Bit-exact NaN payloads are preserved.descendingparameter forargselectAdded
bool descending = falsetoargselect, completing its API to matchargsortandqselect. Whendescending=true:arrsize-1-kthen reverses; scalar path selects astd::greatercomparatorTests
qsort,argsort, andqselecttest_argselectwith four tests: ascending, descending, trailing NaNs, leading NaNsutils/custom-compare.hwith NaN-aware comparators shared across tests