Repository: pandas-dev/pandas

Issue:
ENH: Index should have diff() method



Discussion:
Also `round`
In addition to numeric indices, this would also be useful for datetimelike indices (and avoid a `.to_series()` call to just calculate the diff)
Hi @jorisvandenbossche ; can you explain briefly what this issue is about?
I'm thinking of contributing to this. It would be my first contribution to Pandas :)

Is the correct place to implement this, in `pandas/core/base.py` inside the `IndexOpsMixin` class? Should this use the `diff` function of `pandas/core/algorithms.py`?

Thanks in advance.
> Is the correct place to implement this, in pandas/core/base.py inside the IndexOpsMixin class?

Probably on Index.

> Should this use the diff function of pandas/core/algorithms.py?

Either that or to something like `return Index(self.to_series().diff(n))`
take
I have a WIP version of this here: https://github.com/pandas-dev/pandas/compare/main...jppcr:issue-19708-indexdiff

I ended up using the simpler code that @jbrockmendel proposed, `Index(self.to_series().diff(n))`.

May I ask for guidance in two things?

1) I added a single simple test, that calculates the difference for two integers. Does it make sense to test for more cases (such as for different Index types), since that would just duplicate the original `Series.diff` tests in a different place?

2) For some of the Index types I think it does not make sense at all to implement the diff() function (MultiIndex, IntervalIndex), and for others (TimedeltaIndex, PeriodIndex), more work would be needed than just using `Series.diff` in the background. Would it be OK if I just added a check for the type of `self`, and raised an TypeError if a certain Index type did not support the diff function?

Below are the Index types I tested the function with:
- RangeIndex, Int64Index, UInt64Index, Float64Index - works OK
- CategoricalIndex - works OK if the categories are numerical, but gives out an error if the categories are of mixed types or strings
- IntervalIndex - does not work
- MultiIndex - does not work
- DateTimeIndex - returns Timedeltas, which makes sense
- TimedeltaIndex - does not work as expected. I think for this to work as expected you would need to be able to pass to the  `periods` argument a value such as "1 day", or something like that
- PeriodIndex - also does not work as expected

Thanks
> Me: Would it be OK if I just added a check for the type of self, and raised an TypeError if a certain Index type did not support the diff function?

Specifically, I meant checking if `self` is of type Range/Int64/Uint64/Float64 or DateTimeIndex. If it isn't, I would raise an TypeError exception saying that using `diff` with that Index type isn't supported.
> 
Me: Would it be OK if I just added a check for the type of self, and raised an TypeError if a certain Index type did not support the diff function?

Specifically, I meant checking if self is of type Range/Int64/Uint64/Float64 or DateTimeIndex. If it isn't, I would raise an TypeError exception saying that using diff with that Index type isn't supported.

Shouldn't be necessary, you should get an appropriate exception from the Series.diff method.  Could special-case RangeIndex/MultiIndex to fast-path.

> Does it make sense to test for more cases

Yes.  You can use the `index` fixture.

> TimedeltaIndex - does not work as expected. I think for this to work as expected you would need to be able to pass to the periods argument a value such as "1 day", or something like that

`pd.timedelta_range("1 Day", periods=3).to_series().diff()` looks fine to me.
take

Patch:
diff --git a/doc/source/whatsnew/v2.1.0.rst b/doc/source/whatsnew/v2.1.0.rst
index e860d59f2e5bd..f32598e72685e 100644
--- a/doc/source/whatsnew/v2.1.0.rst
+++ b/doc/source/whatsnew/v2.1.0.rst
@@ -80,6 +80,7 @@ Other enhancements
 - :meth:`MultiIndex.sortlevel` and :meth:`Index.sortlevel` gained a new keyword ``na_position`` (:issue:`51612`)
 - :meth:`arrays.DatetimeArray.map`, :meth:`arrays.TimedeltaArray.map` and :meth:`arrays.PeriodArray.map` can now take a ``na_action`` argument (:issue:`51644`)
 - :meth:`arrays.SparseArray.map` now supports ``na_action`` (:issue:`52096`).
+- Add :meth:`diff()` and :meth:`round()` for :class:`Index` (:issue:`19708`)
 - Add dtype of categories to ``repr`` information of :class:`CategoricalDtype` (:issue:`52179`)
 - Added to the escape mode "latex-math" preserving without escaping all characters between "\(" and "\)" in formatter (:issue:`51903`)
 - Adding ``engine_kwargs`` parameter to :meth:`DataFrame.read_excel` (:issue:`52214`)
diff --git a/pandas/core/indexes/base.py b/pandas/core/indexes/base.py
index 0a56fa4d031d6..60b4bfd65c39d 100644
--- a/pandas/core/indexes/base.py
+++ b/pandas/core/indexes/base.py
@@ -6758,6 +6758,59 @@ def infer_objects(self, copy: bool = True) -> Index:
             result._references.add_index_reference(result)
         return result
 
+    def diff(self, periods: int = 1):
+        """
+        Computes the difference between consecutive values in the Index object.
+
+        If periods is greater than 1, computes the difference between values that
+        are `periods` number of positions apart.
+
+        Parameters
+        ----------
+        periods : int, optional
+            The number of positions between the current and previous
+            value to compute the difference with. Default is 1.
+
+        Returns
+        -------
+        Index
+            A new Index object with the computed differences.
+
+        Examples
+        --------
+        >>> import pandas as pd
+        >>> idx = pd.Index([10, 20, 30, 40, 50])
+        >>> idx.diff()
+        Index([nan, 10.0, 10.0, 10.0, 10.0], dtype='float64')
+
+        """
+        return self._constructor(self.to_series().diff(periods))
+
+    def round(self, decimals: int = 0):
+        """
+        Round each value in the Index to the given number of decimals.
+
+        Parameters
+        ----------
+        decimals : int, optional
+            Number of decimal places to round to. If decimals is negative,
+            it specifies the number of positions to the left of the decimal point.
+
+        Returns
+        -------
+        Index
+            A new Index with the rounded values.
+
+        Examples
+        --------
+        >>> import pandas as pd
+        >>> idx = pd.Index([10.1234, 20.5678, 30.9123, 40.4567, 50.7890])
+        >>> idx.round(decimals=2)
+        Index([10.12, 20.57, 30.91, 40.46, 50.79], dtype='float64')
+
+        """
+        return self._constructor(self.to_series().round(decimals))
+
     # --------------------------------------------------------------------
     # Generated Arithmetic, Comparison, and Unary Methods
 


Test patch:
diff --git a/pandas/tests/indexes/test_base.py b/pandas/tests/indexes/test_base.py
index e681223933abb..db317a819c520 100644
--- a/pandas/tests/indexes/test_base.py
+++ b/pandas/tests/indexes/test_base.py
@@ -1270,6 +1270,38 @@ def test_sortlevel_na_position(self):
         expected = Index([np.nan, 1])
         tm.assert_index_equal(result, expected)
 
+    @pytest.mark.parametrize(
+        "periods, expected_results",
+        [
+            (1, [np.nan, 10, 10, 10, 10]),
+            (2, [np.nan, np.nan, 20, 20, 20]),
+            (3, [np.nan, np.nan, np.nan, 30, 30]),
+        ],
+    )
+    def test_index_diff(self, periods, expected_results):
+        # GH#19708
+        idx = Index([10, 20, 30, 40, 50])
+        result = idx.diff(periods)
+        expected = Index(expected_results)
+
+        tm.assert_index_equal(result, expected)
+
+    @pytest.mark.parametrize(
+        "decimals, expected_results",
+        [
+            (0, [1.0, 2.0, 3.0]),
+            (1, [1.2, 2.3, 3.5]),
+            (2, [1.23, 2.35, 3.46]),
+        ],
+    )
+    def test_index_round(self, decimals, expected_results):
+        # GH#19708
+        idx = Index([1.234, 2.345, 3.456])
+        result = idx.round(decimals)
+        expected = Index(expected_results)
+
+        tm.assert_index_equal(result, expected)
+
 
 class TestMixedIntIndex(Base):
     # Mostly the tests from common.py for which the results differ