Repository: ericmjl/pyjanitor (python)

Issue:
Add the ability to test and sort timestamps to be monotonic in a pandas data frame
# Brief Description

Following up on #703, this issue seeks to introduce the ability to sort the timestamps in a pandas data frame monotonically

I would like to propose...

# Example API
```python
def _test_for_monotonicity(
        df: pd.DataFrame,
        column_name: str = None,
        direction: str = 'increasing'
) -> bool:
    """
    Tests input data frame for monotonicity.

    Check if the data is monotonically increasing or decreasing.
    Direction is dependent on user input.
    Defaults to increasing

    :param df: data frame to be tested for monotonicity
    :param column_name: needs to be specified if and only if the date time is not in index.
        Defaults to None.
    :param direction: specifies the direction in which monotonicity is being tested for.
        Defaults to 'increasing'
    :return: single boolean flag indicating whether the test has passed or not
    """

def sort_monotonically(
        df: pd.DataFrame,
        column_name: str = None,
        direction: str ='increasing'
) -> pd.DataFrame:
    """
    Sorts data frame monotonically.
    It assumes the data frame has an index of type pd.DateTimeIndex when index is datetime.
    If datetime is in a column, then the column is expected to be of type pd.Timestamp

    :param df: data frame to sort monotonically
    :param column_name: needs to be specified if and only if the date time is not in index.
        Defaults to None
    :param direction: specifies the direction in which monotonicity is being tested for.
        Defaults to 'increasing'
    :return: data frame with its index sorted
    """


# more examples below
# ...
```


Pull request description:
[ENH] Add sort_timestamps_monotonically
# PR Description

This PR adds back the `sort_timestamps_monotonically` functionality from PR #720. In that PR, the source branch was removed, so to salvage the PR, I manually added back the functions in this PR.

To fix the failing tests on #720, I also reverted the `pd.assert_frame_equal` to `assert df.equals(other_df)`. Better this than letting the PR linger in limbo.

**This PR resolves #707 and closes #720.**

Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 4b64fc2..f301a9f 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -1,6 +1,6 @@
 new version (on deck)
 =====================
-
+- [ENH] Added function ``sort_timestamps_monotonically`` to timeseries functions @UGuntupalli
 
 v0.20.9
 =======
diff --git a/janitor/timeseries.py b/janitor/timeseries.py
index 5e1a99d..e36d8d1 100644
--- a/janitor/timeseries.py
+++ b/janitor/timeseries.py
@@ -87,3 +87,58 @@ def _get_missing_timestamps(
     missing_timestamps = expected_df.index.difference(df.index)
 
     return expected_df.loc[missing_timestamps]
+
+
+@pf.register_dataframe_method
+def sort_timestamps_monotonically(
+    df: pd.DataFrame, direction: str = "increasing", strict: bool = False
+) -> pd.DataFrame:
+    """
+    Sort dataframe such that index is monotonic.
+
+    If timestamps are monotonic,
+    this function will return the dataframe unmodified.
+    If timestamps are not monotonic,
+    then the function will sort the dataframe.
+
+    Example usage:
+
+    .. code-block:: python
+
+        df = (
+            pd.DataFrame(...)
+            .sort_timestamps_monotonically(direction='increasing')
+        )
+
+    :param df: Dataframe which needs to be tested for monotonicity
+    :param direction: type of monotonicity desired.
+        Acceptable arguments are:
+            1. increasing
+            2. decreasing
+    :param strict: flag to enable/disable strict monotonicity.
+        If set to True,
+        will remove duplicates in the index,
+        by retaining first occurrence of value in index.
+        If set to False,
+        will not test for duplicates in the index.
+        Defaults to False.
+    :returns: Dataframe that has monotonically increasing
+        (or decreasing) timestamps.
+    """
+    # Check all the inputs are the correct data type
+    check("df", df, [pd.DataFrame])
+    check("direction", direction, [str])
+    check("strict", strict, [bool])
+
+    # Remove duplicates if requested
+    if strict:
+        df = df[~df.index.duplicated(keep="first")]
+
+    # Sort timestamps
+    if direction == "increasing":
+        df = df.sort_index()
+    else:
+        df = df.sort_index(ascending=False)
+
+    # Return the dataframe
+    return df


Test patch:
diff --git a/tests/timeseries/test_sort_timestamps_monotonically.py b/tests/timeseries/test_sort_timestamps_monotonically.py
new file mode 100644
index 0000000..813fffa
--- /dev/null
+++ b/tests/timeseries/test_sort_timestamps_monotonically.py
@@ -0,0 +1,52 @@
+import pytest
+import pandas as pd
+from random import randint
+import janitor  # noqa: F401
+import janitor.timeseries  # noqa: F401
+
+
+@pytest.fixture
+def timeseries_dataframe() -> pd.DataFrame:
+    """
+    Returns a time series dataframe
+    """
+    ts_index = pd.date_range("1/1/2019", periods=1000, freq="1H")
+    v1 = [randint(1, 2000) for i in range(1000)]
+    test_df = pd.DataFrame({"v1": v1}, index=ts_index)
+    return test_df
+
+
+# NOTE: The tests possibly can be merged back together later
+# if they are parametrized properly.
+# NOTE: These tests use `df.equals(other_df)`,
+# because the desired `pd.assert_frame_equal(df, other_df)`
+# constantly failed on the CI systems.
+# It's a task for later to fix.
+
+
+@pytest.mark.timeseries
+def test_sort_timestamps_monotonically(timeseries_dataframe):
+    """Test sort_timestamps_monotonically for ascending order"""
+    df = timeseries_dataframe.shuffle(
+        reset_index=False
+    ).sort_timestamps_monotonically()
+    assert df.equals(timeseries_dataframe)
+
+
+@pytest.mark.timeseries
+def test_sort_timestamps_monotonically_decreasing(timeseries_dataframe):
+    """Test sort_timestamps_monotonically for descending order"""
+    df2 = timeseries_dataframe.sort_index(ascending=False)
+    df3 = df2.sort_timestamps_monotonically("decreasing")
+    assert df3.equals(df2)
+
+
+@pytest.mark.timeseries
+def test_sort_timestamps_monotonically_strict(timeseries_dataframe):
+    """Test sort_timestamps_monotonically for index duplication handling"""
+    df = timeseries_dataframe.shuffle(reset_index=False)
+    random_number = randint(1, len(timeseries_dataframe))
+    df = df.append(
+        df.loc[df.index[random_number], :]
+    ).sort_timestamps_monotonically(direction="increasing", strict=True)
+    assert df.equals(timeseries_dataframe)