Skip to content

nearest_ranges: preserve_input_order is ignored for direction="upstream"/"downstream" #169

Description

@marco-mariotti

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions