Skip to content

searchsorted should get a axis kwarg #4224

Open

Description

@andreas-h

Is there a technical reason why searchsorted only works with 1d arrays? In a current problem, I have an array v.shape = (61, 160, 320) and an array a.shape = (1, 160, 320). It would be great if I could do np.searchsorted(a, v, axis=0) to yield a (1, 160, 320) array of the searched indices.

Activity

  1. charris commented on Feb 15, 2014

    @charris
    Member

    @jaimefrio Any thoughts about this?

  2. jaimefrio commented on Feb 15, 2014

    @jaimefrio
    Member

    I think it makes a lot of sense. I have a PR in the works that adds an axis argument to np.bincount, which is being my crash course on NpyIter and nested iteration. Much of what I am doing there should be relatively easy to apply to np.searchsorted, especially now that the new binary search functions can take strided arrays for all three arguments.

    There may be a few quirks to iron out with the UI: broadcasting mixed with an axis argument can get tricky and confusing very fast. I'll ping the list when I have looked into it with a little more care.

  3. andreas-h commented on Mar 24, 2015

    @andreas-h
    ContributorAuthor

    @jaimefrio any news relevant to this issue?

  4. jaimefrio commented on Mar 24, 2015

    @jaimefrio
    Member

    Unfortunately there aren't many good news. We have discussed this elsewhere, but my PR for bincount derailed (#4330) because, even though it worked, it added over 600 lines of code to a function that is barely over 300 right now. So the way to go doesn't seem to be to just multiply the size of every function that could use an axis argument by 3, but rather to build a middle layer that abstracts the iterator complexity, and can be reused in all those functions.

    I might try to get this started in the next few days by sending a longish proposal to the mailing list.

  5. AlJohri commented on Jan 19, 2016

    @AlJohri

    just dropping a note that I came across a need for the axis argument as well. want to perform searchsorted by row over a large matrix

    thanks for your hard work! 👍

  6. rob-smallshire commented on May 18, 2017

    @rob-smallshire

    +1 on a need for this. Major performance bottleneck in my application.

  7. SreenivasVRao commented on Apr 14, 2018

    @SreenivasVRao

    Any progress with this? I'm also looking for an axis argument to searchsorted

  8. altaetran commented on Apr 21, 2018

    @altaetran

    Definitely looking for the same

  9. eric-wieser commented on Apr 21, 2018

    @eric-wieser
    Member

    @mhvk: This might be a good place to expose the new gufunc axis arguments

  10. mhvk commented on Apr 22, 2018

    @mhvk
    Contributor

    It would be lovely to expose gufunc arguments, but I don't think the binsearch functions are gufuncs, or are they?

  11. charris commented on Apr 23, 2018

    @charris
    Member

    Everything in numpy/core/src/npysort is put into a npysort.a library, which is then linked with multiarray. It should not be too difficult to link them into gufuncs, but the sorting functions at least require contiguous inputs. I've thought of adding a step argument, but suspect that there is a tradeoff between speed of access and the overhead of making a copy. Having a gufunc is not quite the same as having an arbitrary axis, but would allow working with stacks of vectors. One difficulty would be if we wanted to broadcast the keys. The output dimensions would also be determined by the keys, which would usually not have dimensions compatible with the searched data, so that could be interesting.

  12. eric-wieser commented on Apr 23, 2018

    @eric-wieser
    Member

    Having a gufunc is not quite the same as having an arbitrary axis,

    It is now that @mhvk allowed gufuncs to specify axis arguments

    One difficulty would be if we wanted to broadcast the keys.

    Isn't this just (i),(j)->(i)?

  13. 20 remaining items

  14. MaanasArora commented on Jul 23, 2026

    @MaanasArora
    Contributor

    The reason I see searchsorted as different from sort/argsort and partition/argpartition is that those functions do not have a second array coming in that we want to broadcast.

    Still have to catch up on the whole discussion, but I think Marten mostly meant ArrayMethods in the dtype sense - I don't think that precludes the use of gufuncs per se! In fact the (arg)partition spec does take the kth index as an additional array argument exactly as in a gufunc. We do some processing on the ktharray manually, so it is true that the broadcasting logic may be more gufunc-native for searchsorted.

    For consistency, we could use very similar dtype dispatching logic as in sorting / partitions, instead of relying entirely on a gufunc as the entrypoint, which does not mean we cannot use a gufunc entirely under the hood (in fact I think we should, to the extent possible). That would include having slots on the dtype and keeping the PyArray_SearchSorted function logic we currently have. It will let us not touch the legacy layer in a way that mirrors the sorts (which also use the ArrFunc_Compare slot). I think could be nicely flexible, both on the legacy and interoperability end, to keep gufuncs as an internal implementation layer rather than a direct Python layer (or even do both and interleave in C if you like having a gufunc anyway).

  15. mhvk commented on Jul 24, 2026

    @mhvk
    Contributor

    @ikrommyd - just a quick note on the shapes - with np.atleast_1d(), I think you'd indeed need to squeeze the last axis if a scalar was passed in, if only to remain completely backwards compatible.

    @jakevdp - with the gufunc axes argument, one has full control over which axis of each input get used, and where the axis gets put in the output. As noted, for numpy by default these are always the last axis, so it would be [-1, -1, -1] in this case. EDIT: so I think one can cover all cases you listed EDIT except the multiple dimensions in one go.

    What we do need to decide, is what axis would mean. In the gufunc signature, it is only allowed if there is only a single shared dimension for all arguments (say, (n),(n)->()), while in this case there would at least have to be an exception for scalar v.

  16. ikrommyd commented on Jul 24, 2026

    @ikrommyd
    Member

    I don't see how one covers this case from jax with axes automatically. It needs 3 loop dims while operands give at most 2 no? I may be stupid here though

    >>> searchsorted(x, y_batched, dimension=2).shape  # search all values along last dimension
    (2, 3, 2, 5)
    

    You can achieve it with some python wrapping creating the new dimensions.

  17. mdhaber commented on Jul 24, 2026

    @mdhaber
    Contributor

    I haven't thought about this for a while, but I appreciate that it's happening. I'd prefer that the behavior of array-api-extra's search sorted be the default and suggest that users can reshape as needed to make it work without additional arguments, but as long as the array-api-extra behavior is achievable, I'm happy.

  18. mhvk commented on Jul 24, 2026

    @mhvk
    Contributor

    @ikrommyd - you're right, one cannot do that case without a reshape. I think we've at some point discussed expanding a single axis to multiple ones (as can be done with np.sum(a, axis=(2,3)) but that is not possible for gufuncs.

  19. mdhaber commented on Aug 15, 2026

    @mdhaber
    Contributor

    @ikrommyd Thought I'd check in. How is this going?

    I wanted to mention more specifically that the (n),(m?),->(m?) behavior is exactly what we did in array-api-extra's searchsorted in hope of getting other libraries and the standard to adopt it.1 I've worked a lot with gufuncs (e.g. added batch support to all of scipy.linalg) and I think it is the natural choice.

    Adding an axes argument to NumPy might be fine, but I wouldn't let interface concerns about it hold things up. Treating the last axis of each array as the core dimension is sufficient, and I doubt axes would end up in the first pass of the standard, since there's not a ton of precedent for that keyword.

    Footnotes

    1. The only difference is that we required the dimensions other than the last to be the same shape before calling the function. However, the broadcasting behavior is well defined and great to include in NumPy. I can't remember exactly why we didn't include it, but it's probably to avoid having to document how it works (e.g. remove core shapes, broadcast batch shapes, prepend to core shapes) and IIRC because there are other functions like it in which the Array API could broadcast but doesn't (e.g. concat). ↩

  20. ikrommyd commented on Aug 18, 2026

    @ikrommyd
    Member

    I waited a bit in case people had more opinions and I had also sent a message to the mailing list but I'm working on an implementation this week.

  21. lucascolley commented on Aug 20, 2026

    @lucascolley
    Contributor
  22. ikrommyd commented on Aug 21, 2026

    @ikrommyd
    Member

    So in the last numpy triage meeting, we discussed considering not exposing an axis argument but going the same route as the array_api_extra and doing it along the last axis and having the user be responsible of swapping the axes if they want. What do you people think of that approach? I'm mainly asking @mdhaber here because you mentioned earlier that this would unblock adding an axis argument in scipy stats functions. Also the original issue from 2014 here is actually asking for searching along axis=0.
    I'm just trying to figure out what use cases are there and whether last axis only enables all of them and whether there is actually a need for exposing axis.

  23. mhvk commented on Aug 21, 2026

    @mhvk
    Contributor

    I think as long as we use a gufunc underneath, I'm fine. If we cannot yet match even the fanciest batching, that seems OK.

  24. ikrommyd commented on Aug 21, 2026

    @ikrommyd
    Member

    Yeah a gufunc would be underneath and that's good for user dtypes too as they can register the loop to support ND searchsorted. I'm only talking about exposing the axis kwarg or forcing it internally to be axis=-1 basically. If axis=-1 is like 99% of the use cases, we may not want to expose it for example. The core work of turning it into a gufunc is in my draft PR already.

  25. MaanasArora commented on Aug 21, 2026

    @MaanasArora
    Contributor

    I agree using a gufunc underneath is sufficient for now; I'd just design the axis semantics such that we can eventually expose axis= to Python as well. Based on what Matt said above about SciPy use-cases it already seems clearly worth it to me. I don't mind doing a little reshaping on the Python end for legacy compatibility eventually if needed. Also, this helps divide up the PRs for easier review!

    Edit: I mean that, by looking at many scipy.interpolate, scipy.integrate functions, and other modules, axis= is passed as an argument. (See interp1d for example but there are many more of course!) I would imagine it would be annoying to swap axes to work with searchsorted for each, but Matt could speak better to that. Even if it's not particularly annoying here this is quite a bit of precedent that axis= can be useful.

  26. mdhaber commented on Aug 21, 2026

    @mdhaber
    Contributor

    Going without axis/axes is absolutely fine. Thanks!

  27. ikrommyd commented on Aug 25, 2026

    @ikrommyd
    Member

    Going without axis/axes is absolutely fine. Thanks!

    @mdhaber does that mean you'd do moveaxis shenanigans in scipy or does axis=-1 cover all the use-cases you want? 🤣 Cause I care about the difference a lot.

  28. mdhaber commented on Aug 25, 2026

    @mdhaber
    Contributor

    axis=-1 covers all the use cases that can be had. A standard axis argument is always redundant with other features (namely moveaxis and, for tuple axes , reshape) and provided for convenience.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions