Repository: scikit-hep/awkward

Issue:
Implement ptp
Is there a precedent for implementing functions that improve API parity with NumPy?

E.g. a naive `ptp` is something like two lines of code:
```python
@ak._connect._numpy.implements("ptp")
def ptp(arr, axis=None, keepdims=False, initial=None, mask_identity=True):
    """Return the range of values along a given axis."""
    return ak.max(
        arr, axis=axis, keepdims=keepdims, initial=initial, mask_identity=mask_identity
    ) - ak.min(
        arr, axis=axis, keepdims=keepdims, initial=initial, mask_identity=mask_identity
    )
```

But it's not an *essential* function. I'd vote in favour of implementing it (in https://github.com/scikit-hep/awkward-1.0/blob/main/src/awkward/operations/reducers.py) because `np.ptp` is more concise and quite widely used.


Discussion:
Improving feature parity with NumPy is definitely desirable, and I'd be grateful for any contributions like the above.

Since you already have an implementation, I'll just tell you where it goes: it's a reducer, so it belongs in `src/awkward/operations/reducers py`, immediately after `min` and `max`. The order of functions in the file matters because it determines the order of functions in the side-bar of the documentation. Like looking for books in a library, a user might be looking for `ak.min` and `ak.max`, but see `ak.ptp` right next to it and go for that.

You already know about the `implements` decorator that works make `ak.ptp` override `np.ptp`, so that's good.

You can copy most of the docstring from `min`, weigh gives a standard explanation of the reducer arguments. If you want to add an example illustrating that this is a difference and mention that "ptp" stands for "peak to peak," that would be helpful. Seen someone clicks on the documentation for `ak.ptp`, it will be the only thing on the page—they won't also be seeing the other reducers for context.

It should also have a test, numbered fit this issue or the PR, that includes comparisons of flat `ak.Arrays` with their corresponding `np.ndarrays`, since they have to behave the same when they coincide. You should make sure that the results are the same for all `axis` values, since `axis != -1` cuts across nested arrays in surprising ways.
Thanks Jim. Let me know if these "discussion issues" are better done after creating a PR in future :)
No, this is exactly right: an issue labeled "feature" is the first step in adding a feature. I'd also accept an appropriate PR without the feature request issue, but raising the issue first gives us a chance to talk it over first, so that you don't put a lot of effort into a PR and then find out that I can't accept it for some reason.

Also, I gave all the details in my previous response though I know you're aware of some of them because someone else might discover this thread in the future when they want to add a feature. It's hard to get exactly the right information into the documentation or CONTRIBUTING.md that each person would need, but if I get enough information out there in the form of answering specific questions like this one, the chances are better that someone who needs it will find it.

Resolving patch:
diff --git a/src/awkward/operations/reducers.py b/src/awkward/operations/reducers.py
--- a/src/awkward/operations/reducers.py
+++ b/src/awkward/operations/reducers.py
@@ -620,6 +620,57 @@ def reduce(xs):
         )
 
 
+@ak._connect._numpy.implements("ptp")
+def ptp(arr, axis=None, keepdims=False, mask_identity=True):
+    """
+    Args:
+        array: Data from which to find the range of values.
+        axis (None or int): If None, combine all values from the array into
+            a single scalar result; if an int, group by that axis: `0` is the
+            outermost, `1` is the first level of nested lists, etc., and
+            negative `axis` counts from the innermost: `-1` is the innermost,
+            `-2` is the next level up, etc.
+        keepdims (bool): If False, this reducer decreases the number of
+            dimensions by 1; if True, the reduced values are wrapped in a new
+            length-1 dimension so that the result of this operation may be
+            broadcasted with the original array.
+        mask_identity (bool): If True, reducing over empty lists results in
+            None (an option type); otherwise, reducing over empty lists
+            results in the operation's identity of 0.
+
+    Returns the range of values in each group of elements from `array` (many
+    types supported, including all Awkward Arrays and Records). The range of
+    an empty list is None, unless `mask_identity=False`, in which case it is 0.
+    This operation is the same as NumPy's
+    [ptp](https://docs.scipy.org/doc/numpy/reference/generated/numpy.ptp.html)
+    if all lists at a given dimension have the same length and no None values,
+    but it generalizes to cases where they do not.
+
+    For example, with an `array` like
+
+        ak.Array([[0, 1, 2, 3],
+                  [          ],
+                  [4, 5      ]])
+
+    The range of the innermost lists is
+
+        >>> ak.ptp(array, axis=-1)
+        <Array [3, None, 1] type='3 * ?float64'>
+
+    because there are three lists, the first has a range of `3`, the second is
+    empty, and the third has a range of `1`.
+
+    See #ak.sum for a more complete description of nested list and missing
+    value (None) handling in reducers.
+    """
+    ptp = ak.max(arr, axis=axis, keepdims=keepdims) - ak.min(
+        arr, axis=axis, keepdims=keepdims
+    )
+    if not mask_identity:
+        return ak.fill_none(ptp, 0)
+    return ptp
+
+
 @ak._connect._numpy.implements("argmin")
 def argmin(array, axis=None, keepdims=False, mask_identity=True):
     """


Test patch:
diff --git a/tests/test_0889-ptp.py b/tests/test_0889-ptp.py
new file mode 100644
--- /dev/null
+++ b/tests/test_0889-ptp.py
@@ -0,0 +1,33 @@
+# BSD 3-Clause License; see https://github.com/scikit-hep/awkward-1.0/blob/main/LICENSE
+
+from __future__ import absolute_import
+
+import pytest  # noqa: F401
+import numpy as np  # noqa: F401
+import awkward as ak  # noqa: F401
+
+
+def test_regular():
+    np_data = np.array(
+        [[1, 3, 5, 4, 2], [3, 7, 8, 2, 4], [2, 3, 1, 7, 7], [5, 1, 9, 10, 2]]
+    )
+    ak_data = ak.from_numpy(np_data)
+
+    assert ak.ptp(ak_data, axis=1).tolist() == np.ptp(np_data, axis=1).tolist()
+    assert ak.ptp(ak_data, axis=0).tolist() == np.ptp(np_data, axis=0).tolist()
+    assert ak.ptp(ak_data) == np.ptp(np_data)
+
+
+def test_jagged():
+    data = ak.Array(
+        [
+            [1, 3, 5, 4, 2],
+            [],
+            [2, 3, 1],
+            [5],
+        ]
+    )
+    assert ak.ptp(data, axis=1, mask_identity=False).tolist() == [4, 0, 2, 0]
+    assert ak.ptp(data, axis=1).tolist() == [4, None, 2, 0]
+    assert ak.ptp(data, axis=0).tolist() == [4, 0, 4, 0, 0]
+    assert ak.ptp(data) == 4