Repository: pyjanitor-devs/pyjanitor

Issue:
[ENH] Send row or column to an arbitrary position in the sequence of rows/columns
# Brief Description

I would like to propose a new function that moves a row (or column) that a user picks to a new position in the sequence of rows/columns.

cc: @bhallaY: Can you fill out an example API?



Discussion:
This could probably be extended to multiple rows trivially, I'm thinking.

Resolving patch:
diff --git a/docs/conf.py b/docs/conf.py
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -10,13 +10,14 @@
 
 # -- Path setup --------------------------------------------------------------
 
+import datetime
+
 # If extensions (or modules to document with autodoc) are in another directory,
 # add these directories to sys.path here. If the directory is relative to the
 # documentation root, use os.path.abspath to make it absolute, like shown here.
 #
 import os
 import sys
-import datetime
 from pathlib import Path
 
 sys.path.insert(0, os.path.abspath("."))
@@ -37,7 +38,7 @@
 
 now = datetime.datetime.now()
 CurrentYear = str(now.year)
-copyright = CurrentYear+", PyJanitor devs"
+copyright = CurrentYear + ", PyJanitor devs"
 author = "Eric J. Ma"
 
 # The short X.Y version
diff --git a/janitor/functions.py b/janitor/functions.py
--- a/janitor/functions.py
+++ b/janitor/functions.py
@@ -25,6 +25,95 @@
 )
 
 
+@pf.register_dataframe_method
+def move(
+    df: pd.DataFrame,
+    source: Union[int, str],
+    target: Union[int, str],
+    position: str = "before",
+    axis: int = 0,
+) -> pd.DataFrame:
+    """
+    Move column or row to a position adjacent to another column or row in
+    dataframe. Must have unique column names or indices.
+
+    This operation does not reset the index of the dataframe. User must
+    explicitly do so.
+
+    Does not apply to multilevel dataframes.
+
+    Functional usage example:
+
+    .. code-block:: python
+
+        df = move(df, source=3, target=15, position='after', axis=0)
+
+    Method chaining example:
+
+    .. code-block:: python
+
+        import pandas as pd
+        import janitor
+        df = pd.DataFrame(...).move(source=3, target=15, position='after',
+        axis=0)
+
+    :param df: The pandas Dataframe object.
+    :param int or str source: column or row to move
+    :param str target: column or row to move adjacent to
+    :param str position: Specifies whether the Series is moved to before or
+        after the adjacent Series. Values can be either 'before' or 'after';
+        defaults to 'before'.
+    :param int axis: Axis along which the function is applied. 0 to move a
+        row, 1 to move a column.
+    :returns: The dataframe with the Series moved.
+
+    """
+    if axis not in [0, 1]:
+        raise ValueError(f"Invalid axis '{axis}'. Can only be 0 or 1.")
+
+    if position not in ["before", "after"]:
+        raise ValueError(
+            f"Invalid position '{position}'. Can only be 'before' or 'after'."
+        )
+
+    if axis == 0:
+        names = list(df.index)
+
+        if source not in names:
+            raise ValueError(f"Source row '{source}' not in dataframe.")
+
+        if target not in names:
+            raise ValueError(f"Target row '{target}' not in dataframe.")
+
+        names.remove(source)
+        pos = names.index(target)
+
+        if position == "after":
+            pos += 1
+        names.insert(pos, source)
+
+        df = df.loc[names, :]
+    else:
+        names = list(df.columns)
+
+        if source not in names:
+            raise ValueError(f"Source column '{source}' not in dataframe.")
+
+        if target not in names:
+            raise ValueError(f"Target column '{target}' not in dataframe.")
+
+        names.remove(source)
+        pos = names.index(target)
+
+        if position == "after":
+            pos += 1
+        names.insert(pos, source)
+
+        df = df.loc[:, names]
+
+    return df
+
+
 @pf.register_dataframe_method
 def clean_names(
     df: pd.DataFrame,


Test patch:
diff --git a/tests/functions/test_move.py b/tests/functions/test_move.py
new file mode 100644
--- /dev/null
+++ b/tests/functions/test_move.py
@@ -0,0 +1,128 @@
+import pytest
+from pandas import testing
+
+
+@pytest.mark.functions
+def test_move_row(dataframe):
+    """
+    Test function move() for rows with defaults.
+    Case with row labels being integers.
+    """
+    # Setup
+    source = 1
+    target = 3
+    row = dataframe.loc[source, :]
+
+    # Exercise
+    result = dataframe.move(source=source, target=target, axis=0)
+
+    # Verify
+    testing.assert_series_equal(result.iloc[target - 1, :], row)
+
+
+@pytest.mark.functions
+def test_move_row_after(dataframe):
+    """
+    Test function move() for rows with position = 'after'.
+    Case with row labels being integers.
+    """
+    # Setup
+    source = 1
+    target = 3
+    row = dataframe.loc[source, :]
+
+    # Exercise
+    result = dataframe.move(
+        source=source, target=target, position="after", axis=0
+    )
+
+    # Verify
+    testing.assert_series_equal(result.iloc[target, :], row)
+
+
+@pytest.mark.functions
+def test_move_row_strings(dataframe):
+    """
+    Test function move() for rows with defaults.
+    Case with row labels being strings.
+    """
+    # Setup
+    dataframe = dataframe.set_index("animals@#$%^").drop_duplicates()
+    rows = dataframe.index
+    source_index = 1
+    target_index = 2
+    source = rows[source_index]
+    target = rows[target_index]
+    row = dataframe.loc[source, :]
+
+    # Exercise
+    result = dataframe.move(source=source, target=target, axis=0)
+
+    # Verify
+    testing.assert_series_equal(result.iloc[target_index - 1, :], row)
+
+
+@pytest.mark.functions
+def test_move_row_after_strings(dataframe):
+    """
+    Test function move() for rows with position = 'after'.
+    Case with row labels being strings.
+    """
+    # Setup
+    dataframe = dataframe.set_index("animals@#$%^").drop_duplicates()
+    rows = dataframe.index
+    source_index = 1
+    target_index = 2
+    source = rows[source_index]
+    target = rows[target_index]
+    row = dataframe.loc[source, :]
+
+    # Exercise
+    result = dataframe.move(
+        source=source, target=target, position="after", axis=0
+    )
+
+    # Verify
+    testing.assert_series_equal(result.iloc[target_index, :], row)
+
+
+@pytest.mark.functions
+def test_move_col(dataframe):
+    """
+    Test function move() for columns with defaults.
+    """
+    # Setup
+    columns = dataframe.columns
+    source_index = 1
+    target_index = 3
+    source = columns[source_index]
+    target = columns[target_index]
+    col = dataframe[source]
+
+    # Exercise
+    result = dataframe.move(source=source, target=target, axis=1)
+
+    # Verify
+    testing.assert_series_equal(result.iloc[:, target_index - 1], col)
+
+
+@pytest.mark.functions
+def test_move_col_after(dataframe):
+    """
+    Test function move() for columns with position = 'after'.
+    """
+    # Setup
+    columns = dataframe.columns
+    source_index = 1
+    target_index = 3
+    source = columns[source_index]
+    target = columns[target_index]
+    col = dataframe[source]
+
+    # Exercise
+    result = dataframe.move(
+        source=source, target=target, position="after", axis=1
+    )
+
+    # Verify
+    testing.assert_series_equal(result.iloc[:, target_index], col)