nearest_ranges takes ties - #172
Merged
Merged
Conversation
Overlapping intervals are all at distance 0, so with the default exclude_overlaps=False "every tied interval" means "every interval of other that self overlaps". One interval of self covered by 500 intervals of other produces 500 rows, every one of them equally the nearest. On 100 million hg38-like intervals against themselves that is 1.1 billion output rows against 7.7 million for one per query -- 144x, and the difference between fitting in memory and not. `ties="first"` reports one interval per distance instead, so each interval of self appears at most once. Which one is unspecified, as it is for bedtools `closest -t first`, GenomicRanges `select="arbitrary"` and BEDOPS `--closest`; the same input gives the same answer, and nothing is sorted to decide it because the sort would cost more than the option saves. The default, "all", is unchanged. The tied rows are never built. Filtering afterwards would have cost the same memory as "all" and more time, so the saving comes from the kernel: the overlap sweep stops at the first hit per interval of self, each directional sweep emits one row per distinct distance, and the merge keeps one row per distance bucket. That is `ruranges_core::nearest`, so the floor on ruranges rises to 0.1.8. `k` still counts distinct distances, so `ties="first", k=2` gives two rows rather than being rejected. An unknown value raises ValueError naming the options, like `direction` and `multiple`, rather than reaching the kernel and aborting the interpreter with a Rust panic. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
marco-mariotti
merged commit Aug 14, 2026
01328a4
into
nearest-ranges-strand-direction
4 of 8 checks passed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Repo
pyranges/pyranges1· basenearest-ranges-strand-direction· headnearest-ties· 6 files, +181 −3Why
Overlapping intervals are all at distance 0. With the default
exclude_overlaps=False,"the nearest interval" is therefore every interval of
otherthat a query overlaps — oneinterval of self covered by 500 intervals of other produces 500 rows, all of them equally
the nearest.
That is rarely what a user asking for "the nearest gene" wants, and on real data the cost is
severe. On 100 million hg38-like intervals against themselves:
144x — the difference between fitting in memory and not.
What
On both
PyRanges.nearest_rangesandRangeFrame.nearest_ranges, with the same vocabularythe rest of the field uses: bedtools spells it
closest -t first, GenomicRangesselect="arbitrary", BEDOPS--closest. bioframe, polars-bio and pyranges 0.x report oneinterval per query and offer no way to ask for the other behaviour.
ties="all"is whatnearest_rangeshas always done.answer. Nothing is sorted to decide it, because the sort would cost more than the option
saves.
kstill counts distinct distances, not rows:ties="first", k=2gives two rows, oneper distance, rather than the combination being rejected.
ValueErrornaming the options, asdirectionandmultiplealready do, rather than reaching the kernel — a Rust panic is not an
Exception, soexcept Exceptioncannot catch it and the interpreter is aborted.Where the saving comes from
Not from filtering. Materialising 1.1 billion rows and then keeping one per query costs the
same memory and more time, so the tied rows are never built: the overlap sweep stops at the
first hit per query, each directional sweep emits one row per distinct distance, and the
merge keeps one row per distance bucket. That is
ruranges_core::nearest::nearest_with_ties,so the floor on
rurangesrises to 0.1.8.Version 1.4.2 → 1.4.3.
Tests
Six new tests in
tests/unit/test_nearest_ranges.py(10/10 in the file, 128/128 intests/unit), plus a doctest onPyRanges.nearest_ranges:over
exclude_overlaps ∈ {False, True},ties="first"answers exactly the queriesties="all"answers, at the same distance, with exactly one row each, with an assertionthat the fixture has ties so it cannot go vacuous;
bucket (a neighbour that merely touches is at distance 1, not 0);
k=2reporting one row per distance;the option has to survive both halves and the recombination;
tieson both classes.Doctests: 462 in
pyranges_mainand 13 inrange_framepass.ruff format --checkandruff checkclean.Cross-library
On a tie-rich hand-made pair,
ties="first"puts pyranges1 on the same side of the line asthe libraries that have only ever had one behaviour:
closestnearestnearestties="first"ties="all"(default)