PyRanges.nearest_ranges(direction="upstream"/"downstream") returns rows grouped by strand — all + rows, then all - rows — regardless of preserve_input_order. The argument has no effect at all on a directional query. direction="any" is unaffected.
Reproducer
Four query intervals with interleaved strands:
import pandas as pd
import pyranges1 as pr
query = pr.PyRanges(pd.DataFrame({
"Chromosome": ["chr1"] * 4,
"Start": [100, 300, 500, 700],
"End": [120, 320, 520, 720],
"Strand": ["+", "-", "+", "-"],
"Id": ["a", "b", "c", "d"],
}))
other = pr.PyRanges(pd.DataFrame({
"Chromosome": ["chr1"] * 2,
"Start": [0, 900], "End": [10, 910], "Strand": ["+", "+"],
}))
for direction in ("any", "upstream", "downstream"):
for preserve in (True, False):
result = query.nearest_ranges(
other, direction=direction, strand_behavior="ignore",
preserve_input_order=preserve,
)
print(direction, preserve, result["Id"].tolist(), list(result.index))
any True ['a', 'b', 'c', 'd'] [0, 1, 2, 3]
any False ['a', 'b', 'c', 'd'] [0, 1, 2, 3]
upstream True ['a', 'c', 'b', 'd'] [0, 2, 1, 3] <- grouped by strand
upstream False ['a', 'c', 'b', 'd'] [0, 2, 1, 3]
downstream True ['a', 'c', 'b', 'd'] [0, 2, 1, 3]
downstream False ['a', 'c', 'b', 'd'] [0, 2, 1, 3]
The two preserve_input_order values give identical output for the directional cases, so the option is silently inert there. The returned index is [0, 2, 1, 3] rather than [0, 1, 2, 3], which matters because PyRanges otherwise preserves the input index.
Inconsistent with the sibling methods
Every other binary method taking preserve_input_order honours it on the same interleaved frame:
overlap ['a', 'b', 'c', 'd']
join_overlaps ['a', 'b', 'c', 'd']
intersect_overlaps ['a', 'b', 'c', 'd']
nearest_ranges(up) ['c', 'b', 'd'] <- grouped by strand
Cause
PyRanges.nearest_ranges splits self by strand for a directional query — it has to, because the two strands search opposite coordinate directions — and then recombines with a plain concatenation:
return ensure_pyranges(pd.concat(per_strand))
preserve_input_order is forwarded into each half, so ordering holds within a strand, but nothing restores the interleaving between them.
Suggested fix
Sort the concatenated frame back into input order when preserve_input_order is set. The row's original position has to be carried through the split to do this — the index alone is not enough, since k > 1 duplicates it and callers may supply a non-unique index.
Roughly:
ordered = self.assign(**{ORDER_COL: range(len(self))})
# ... split, query each half, concat ...
if preserve_input_order:
combined = combined.sort_values(ORDER_COL, kind="stable")
return combined.drop(columns=[ORDER_COL])
Note
This changes returned row order for directional queries, so it is user-visible even though it makes the documented option behave as documented. It may deserve its own release note rather than riding along with an unrelated change.
Seen on master (73e86d8); predates #162/#168, which change direction semantics but not ordering.
PyRanges.nearest_ranges(direction="upstream"/"downstream")returns rows grouped by strand — all+rows, then all-rows — regardless ofpreserve_input_order. The argument has no effect at all on a directional query.direction="any"is unaffected.Reproducer
Four query intervals with interleaved strands:
The two
preserve_input_ordervalues give identical output for the directional cases, so the option is silently inert there. The returned index is[0, 2, 1, 3]rather than[0, 1, 2, 3], which matters becausePyRangesotherwise preserves the input index.Inconsistent with the sibling methods
Every other binary method taking
preserve_input_orderhonours it on the same interleaved frame:Cause
PyRanges.nearest_rangessplitsselfby strand for a directional query — it has to, because the two strands search opposite coordinate directions — and then recombines with a plain concatenation:preserve_input_orderis forwarded into each half, so ordering holds within a strand, but nothing restores the interleaving between them.Suggested fix
Sort the concatenated frame back into input order when
preserve_input_orderis set. The row's original position has to be carried through the split to do this — the index alone is not enough, sincek > 1duplicates it and callers may supply a non-unique index.Roughly:
Note
This changes returned row order for directional queries, so it is user-visible even though it makes the documented option behave as documented. It may deserve its own release note rather than riding along with an unrelated change.
Seen on master (
73e86d8); predates #162/#168, which change direction semantics but not ordering.