Skip to content

Commit f898c60

Browse files
MaanasAroraseberg
andauthored
ENH: Descending partitioning (numpy#31511)
This PR adds `descending` as an option to partitioning and expands the internal implementations to support it. User DTypes are not yet supported as they currently always use the sort fallback. Additional changes should allow to (a) use the sort as a fallback in a more reliable way and (b) change partitioning to support a similar new-style API with registration for user DTypes. Co-authored-by: Sebastian Berg <sebastianb@nvidia.com>
1 parent 6e0d988 commit f898c60

14 files changed

Lines changed: 548 additions & 170 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
New ``descending`` keyword argument for `numpy.partition` and `numpy.argpartition`
2+
----------------------------------------------------------------------------------
3+
Users can now pass the ``descending=True`` keyword argument to `numpy.partition` and
4+
`numpy.argpartition` to partition and argpartition arrays in descending order.
5+
NaN values, if present, are partitioned to the end of the array in both ascending and
6+
descending sorts. This feature is available for all built-in dtypes except
7+
``string``, ``unicode``, ``void``, ``object``, and ``generic``. Note that SIMD
8+
optimizations for partitioning are currently not available for descending order,
9+
so performance may be slower.

doc/source/reference/c-api/array.rst

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4677,8 +4677,6 @@ Enumerated Types
46774677
.. c:enumerator:: NPY_SORT_DESCENDING
46784678
46794679
(Requirement) Specifies that the sort must be in descending order.
4680-
This functionality is not yet implemented for any of the NumPy types
4681-
and cannot yet be set from the Python interface.
46824680
46834681
.. c:enum:: NPY_SCALARKIND
46844682
@@ -4759,10 +4757,22 @@ Enumerated Types
47594757
47604758
.. c:enum:: NPY_SELECTKIND
47614759
4762-
A variable type indicating the selection algorithm being used.
4760+
A variable type indicating the selection algorithm options for
4761+
the partitioning functions, see also :c:type:`NPY_SORTKIND`.
4762+
4763+
.. c:enumerator:: NPY_SELECT_DEFAULT
4764+
4765+
The default selection algorithm.
4766+
4767+
.. c:enumerator:: NPY_SELECT_DESCENDING
4768+
4769+
(Requirement) Flag that changes the partition/sort order to descending.
47634770
47644771
.. c:enumerator:: NPY_INTROSELECT
47654772
4773+
Identical to ``NPY_SELECT_DEFAULT`` but defined prior to NumPy 2.5.
4774+
Prefer ``NPY_SELECT_DEFAULT`` if compiling with NumPy 2.5 or later.
4775+
47664776
.. c:enum:: NPY_CASTING
47674777
47684778
An enumeration type indicating how permissive data conversions should

numpy/__init__.pyi

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3614,17 +3614,19 @@ class ndarray(_ArrayOrScalarCommon, Generic[_ShapeT_co, _DTypeT_co]):
36143614
kth: _ArrayLikeInt,
36153615
/,
36163616
axis: SupportsIndex = -1,
3617-
kind: _PartitionKind = "introselect",
3617+
kind: _PartitionKind | None = None,
36183618
order: None = None,
3619+
descending: py_bool | None = None,
36193620
) -> None: ...
36203621
@overload
36213622
def partition(
36223623
self: NDArray[void],
36233624
kth: _ArrayLikeInt,
36243625
/,
36253626
axis: SupportsIndex = -1,
3626-
kind: _PartitionKind = "introselect",
3627+
kind: _PartitionKind | None = None,
36273628
order: str | Sequence[str] | None = None,
3629+
descending: py_bool | None = None,
36283630
) -> None: ...
36293631

36303632
# keep in sync with `ma.core.MaskedArray.argpartition`
@@ -3635,35 +3637,39 @@ class ndarray(_ArrayOrScalarCommon, Generic[_ShapeT_co, _DTypeT_co]):
36353637
kth: _ArrayLikeInt,
36363638
/,
36373639
axis: None,
3638-
kind: _PartitionKind = "introselect",
3640+
kind: _PartitionKind | None = None,
36393641
order: None = None,
3642+
descending: py_bool | None = None,
36403643
) -> ndarray[tuple[int], _dtype[intp]]: ...
36413644
@overload # axis: index (default)
36423645
def argpartition(
36433646
self,
36443647
kth: _ArrayLikeInt,
36453648
/,
36463649
axis: SupportsIndex = -1,
3647-
kind: _PartitionKind = "introselect",
3650+
kind: _PartitionKind | None = None,
36483651
order: None = None,
3652+
descending: py_bool | None = None,
36493653
) -> ndarray[_ShapeT_co, _dtype[intp]]: ...
36503654
@overload # void, axis: None
36513655
def argpartition(
36523656
self: NDArray[void],
36533657
kth: _ArrayLikeInt,
36543658
/,
36553659
axis: None,
3656-
kind: _PartitionKind = "introselect",
3660+
kind: _PartitionKind | None = None,
36573661
order: str | Sequence[str] | None = None,
3662+
descending: py_bool | None = None,
36583663
) -> ndarray[tuple[int], _dtype[intp]]: ...
36593664
@overload # void, axis: index (default)
36603665
def argpartition(
36613666
self: NDArray[void],
36623667
kth: _ArrayLikeInt,
36633668
/,
36643669
axis: SupportsIndex = -1,
3665-
kind: _PartitionKind = "introselect",
3670+
kind: _PartitionKind | None = None,
36663671
order: str | Sequence[str] | None = None,
3672+
descending: py_bool | None = None,
36673673
) -> ndarray[_ShapeT_co, _dtype[intp]]: ...
36683674

36693675
# keep in sync with `ma.MaskedArray.diagonal`

numpy/_core/_add_newdocs.py

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3284,10 +3284,10 @@
32843284

32853285
add_newdoc('numpy._core.multiarray', 'ndarray', ('argpartition',
32863286
"""
3287-
argpartition($self, kth, /, axis=-1, kind='introselect', order=None)
3287+
argpartition($self, kth, /, axis=-1, kind='introselect', order=None, descending=None)
32883288
--
32893289
3290-
a.argpartition(kth, axis=-1, kind='introselect', order=None)
3290+
a.argpartition(kth, axis=-1, kind='introselect', order=None, descending=None)
32913291
32923292
Returns the indices that would partition this array.
32933293
@@ -3302,41 +3302,52 @@
33023302

33033303
add_newdoc('numpy._core.multiarray', 'ndarray', ('partition',
33043304
"""
3305-
partition($self, kth, /, axis=-1, kind='introselect', order=None)
3305+
partition($self, kth, /, axis=-1, kind='introselect', order=None, descending=None)
33063306
--
33073307
3308-
a.partition(kth, axis=-1, kind='introselect', order=None)
3308+
a.partition(kth, axis=-1, kind='introselect', order=None, descending=None)
33093309
3310-
Partially sorts the elements in the array in such a way that the value of
3311-
the element in k-th position is in the position it would be in a sorted
3312-
array. In the output array, all elements smaller than the k-th element
3313-
are located to the left of this element and all equal or greater are
3314-
located to its right. The ordering of the elements in the two partitions
3315-
on the either side of the k-th element in the output array is undefined.
3310+
Partially sorts the array in such a way that the value of the element in the k-th
3311+
position is in the position it would be in a sorted array. In the output array,
3312+
all elements that would be to the left of the k-th element in a sorted array are
3313+
located to the left of this element and all that would be to the right are located
3314+
to its right. The ordering of the elements in the two partitions on the either side
3315+
of the k-th element in the output array is undefined.
33163316
33173317
Parameters
33183318
----------
33193319
kth : int or sequence of ints
3320-
Element index to partition by. The kth element value will be in its
3321-
final sorted position and all smaller elements will be moved before it
3322-
and all equal or greater elements behind it.
3323-
The order of all elements in the partitions is undefined.
3324-
If provided with a sequence of kth it will partition all elements
3325-
indexed by kth of them into their sorted position at once.
3320+
Element index to partition by. The k-th value of the array will
3321+
be in the position it would be in a sorted array, all elements
3322+
that are less than this element (or greater if `descending` is True)
3323+
will be moved before it, and all elements that are greater than or
3324+
equal to this element (or less than or equal if `descending` is True)
3325+
will be moved after it. The order of all elements within each partition
3326+
is undefined. If provided with a sequence of k-th it will partition all
3327+
elements indexed by k-th of them into their sorted position at once.
33263328
33273329
.. deprecated:: 1.22.0
33283330
Passing booleans as index is deprecated.
33293331
axis : int, optional
33303332
Axis along which to sort. Default is -1, which means sort along the
33313333
last axis.
33323334
kind : {'introselect'}, optional
3333-
Selection algorithm. Default is 'introselect'.
3335+
NumPy currently offers only one selection algorithm, 'introselect',
3336+
and this parameter provides no additional functionality. Default
3337+
is ``None``.
33343338
order : str or list of str, optional
33353339
When `a` is an array with fields defined, this argument specifies
33363340
which fields to compare first, second, etc. A single field can
33373341
be specified as a string, and not all fields need to be specified,
33383342
but unspecified fields will still be used, in the order in which
33393343
they come up in the dtype, to break ties.
3344+
descending : bool, optional
3345+
Sort order. If ``True``, the array will be partitioned in
3346+
descending order. If ``False`` or ``None``, the array will be
3347+
partitioned in ascending order. Values that are NaN are partitioned
3348+
towards the end of the array regardless of order. Default: ``None``.
3349+
3350+
.. versionadded:: 2.6.0
33403351
33413352
See Also
33423353
--------

numpy/_core/fromnumeric.py

Lines changed: 66 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -727,46 +727,58 @@ def matrix_transpose(x, /):
727727
return swapaxes(x, -1, -2)
728728

729729

730-
def _partition_dispatcher(a, kth, axis=None, kind=None, order=None):
730+
def _partition_dispatcher(a, kth, axis=None, kind=None, order=None, descending=None):
731731
return (a,)
732732

733733

734734
@array_function_dispatch(_partition_dispatcher)
735-
def partition(a, kth, axis=-1, kind='introselect', order=None):
735+
def partition(a, kth, axis=-1, kind=np._NoValue, order=None, descending=np._NoValue):
736736
"""
737737
Return a partitioned copy of an array.
738738
739739
Creates a copy of the array and partially sorts it in such a way that
740-
the value of the element in k-th position is in the position it would be
741-
in a sorted array. In the output array, all elements smaller than the k-th
742-
element are located to the left of this element and all equal or greater
743-
are located to its right. The ordering of the elements in the two
744-
partitions on the either side of the k-th element in the output array is
745-
undefined.
740+
the value of the element in the k-th position is in the position it would be
741+
in a sorted array. In the output array, all elements that would be to the left
742+
of the k-th element in a sorted array are located to the left of this element and
743+
all that would be to the right are located to its right. The ordering of the
744+
elements in the two partitions on the either side of the k-th element in the
745+
output array is undefined.
746746
747747
Parameters
748748
----------
749749
a : array_like
750750
Array to be sorted.
751751
kth : int or sequence of ints
752-
Element index to partition by. The k-th value of the element
753-
will be in its final sorted position and all smaller elements
754-
will be moved before it and all equal or greater elements behind
755-
it. The order of all elements in the partitions is undefined. If
752+
Element index to partition by. In the returned array, the k-th
753+
value of the array will be in the position it would be in a
754+
sorted array, all elements that are less than this element (or
755+
greater if `descending` is True) will be moved before it, and
756+
all elements that are greater than or equal to this element
757+
(or less than or equal if `descending` is True) will be moved after it.
758+
The order of all elements within each partition is undefined. If
756759
provided with a sequence of k-th it will partition all elements
757-
indexed by k-th of them into their sorted position at once.
760+
indexed by k-th of them into their sorted position at once.
758761
759762
axis : int or None, optional
760763
Axis along which to sort. If None, the array is flattened before
761764
sorting. The default is -1, which sorts along the last axis.
762765
kind : {'introselect'}, optional
763-
Selection algorithm. Default is 'introselect'.
766+
NumPy currently offers only one selection algorithm, 'introselect',
767+
and this parameter provides no additional functionality. Default
768+
is ``None``.
764769
order : str or list of str, optional
765770
When `a` is an array with fields defined, this argument
766771
specifies which fields to compare first, second, etc. A single
767772
field can be specified as a string. Not all fields need be
768773
specified, but unspecified fields will still be used, in the
769774
order in which they come up in the dtype, to break ties.
775+
descending : bool, optional
776+
Sort order. If ``True``, the array will be partitioned in
777+
descending order. If ``False`` or ``None``, the array will be
778+
partitioned in ascending order. Values that are NaN are partitioned
779+
towards the end of the array regardless of order. Default: ``None``.
780+
781+
.. versionadded:: 2.6.0
770782
771783
Returns
772784
-------
@@ -803,7 +815,8 @@ def partition(a, kth, axis=-1, kind='introselect', order=None):
803815
the real parts except when they are equal, in which case the order
804816
is determined by the imaginary parts.
805817
806-
The sort order of ``np.nan`` is bigger than ``np.inf``.
818+
Regardless of sort order, `np.nan` is partitioned to the right of
819+
any other value.
807820
808821
Examples
809822
--------
@@ -839,16 +852,24 @@ def partition(a, kth, axis=-1, kind='introselect', order=None):
839852
axis = -1
840853
else:
841854
a = asanyarray(a).copy(order="K")
842-
a.partition(kth, axis=axis, kind=kind, order=order)
855+
856+
# Sanitize for backward compatibility
857+
kwargs = {}
858+
if descending is not np._NoValue:
859+
kwargs['descending'] = descending
860+
if kind is not np._NoValue:
861+
kwargs['kind'] = kind
862+
863+
a.partition(kth, axis=axis, order=order, **kwargs)
843864
return a
844865

845866

846-
def _argpartition_dispatcher(a, kth, axis=None, kind=None, order=None):
867+
def _argpartition_dispatcher(a, kth, axis=None, kind=None, order=None, descending=None):
847868
return (a,)
848869

849870

850871
@array_function_dispatch(_argpartition_dispatcher)
851-
def argpartition(a, kth, axis=-1, kind='introselect', order=None):
872+
def argpartition(a, kth, axis=-1, kind=np._NoValue, order=None, descending=np._NoValue):
852873
"""
853874
Perform an indirect partition along the given axis using the
854875
algorithm specified by the `kind` keyword. It returns an array of
@@ -860,24 +881,36 @@ def argpartition(a, kth, axis=-1, kind='introselect', order=None):
860881
a : array_like
861882
Array to sort.
862883
kth : int or sequence of ints
863-
Element index to partition by. The k-th element will be in its
864-
final sorted position and all smaller elements will be moved
865-
before it and all larger elements behind it. The order of all
866-
elements in the partitions is undefined. If provided with a
867-
sequence of k-th it will partition all of them into their sorted
868-
position at once.
884+
Element index to partition by. In the returned array, the k-th
885+
value of the array will be in the position it would be in a
886+
sorted array, all elements that are less than this element (or
887+
greater if `descending` is True) will be moved before it, and
888+
all elements that are greater than or equal to this element
889+
(or less than or equal if `descending` is True) will be moved after it.
890+
The order of all elements within each partition is undefined. If
891+
provided with a sequence of k-th it will partition all elements
892+
indexed by k-th of them into their sorted position at once.
869893
870894
axis : int or None, optional
871895
Axis along which to sort. The default is -1 (the last axis). If
872896
None, the flattened array is used.
873897
kind : {'introselect'}, optional
874-
Selection algorithm. Default is 'introselect'
898+
NumPy currently offers only one selection algorithm, 'introselect',
899+
and this parameter provides no additional functionality. Default
900+
is ``None``.
875901
order : str or list of str, optional
876902
When `a` is an array with fields defined, this argument
877903
specifies which fields to compare first, second, etc. A single
878904
field can be specified as a string, and not all fields need be
879905
specified, but unspecified fields will still be used, in the
880906
order in which they come up in the dtype, to break ties.
907+
descending : bool, optional
908+
Sort order. If ``True``, the array will be partitioned in
909+
descending order. If ``False`` or ``None``, the array will be
910+
partitioned in ascending order. Values that are NaN are partitioned
911+
towards the end of the array regardless of order. Default: ``None``.
912+
913+
.. versionadded:: 2.6.0
881914
882915
Returns
883916
-------
@@ -931,7 +964,14 @@ def argpartition(a, kth, axis=-1, kind='introselect', order=None):
931964
[1, 1, 3]])
932965
933966
"""
934-
return _wrapfunc(a, 'argpartition', kth, axis=axis, kind=kind, order=order)
967+
# Sanitize for backward compatibility
968+
kwargs = {}
969+
if descending is not np._NoValue:
970+
kwargs['descending'] = descending
971+
if kind is not np._NoValue:
972+
kwargs['kind'] = kind
973+
974+
return _wrapfunc(a, "argpartition", kth, axis=axis, order=order, **kwargs)
935975

936976

937977
def _sort_dispatcher(

0 commit comments

Comments
 (0)