Repository: pyjanitor-devs/pyjanitor

Issue:
Feature enhancement thought: reorder_columns()
Example usage:

```python
df = (
    pd.read_csv('blah.csv')  # containing ['col1', 'col2']
    .add_column('col3', 12345)
    .reorder_columns(['col3'])  
)
```

Columns not specified retain their order and follow after specified columns


Discussion:
Internally, something like:

```python
column_order = ['col3']

df = df.reindex(columns=(column_order + [col for col in df.columns if col not in column_order]), copy=False)
```

?
Sure stuff! I'd also be happy to accept a PR for this. (If it were two separate PRs, that'd be easier for review.)

Resolving patch:
diff --git a/janitor/functions.py b/janitor/functions.py
--- a/janitor/functions.py
+++ b/janitor/functions.py
@@ -14,7 +14,7 @@
 import pandas_flavor as pf
 
 from .errors import JanitorError
-from typing import List
+from typing import List, Union
 
 
 def _strip_underscores(df, strip_underscores=None):
@@ -352,6 +352,64 @@ def rename_column(df, old, new):
     return df.rename(columns={old: new})
 
 
+@pf.register_dataframe_method
+def reorder_columns(
+    df: pd.DataFrame, column_order: Union[List, pd.Index]
+) -> pd.DataFrame:
+    """
+    Reorder DataFrame columns by specifying desired order as list of col names
+    Columns not specified retain their order and follow after specified cols
+    Validates column_order to ensure columns are all present in DataFrame.
+
+    Functional usage example:
+
+    Given `DataFrame` with column names `col1`, `col2`, `col3`:
+
+    .. code-block:: python
+
+        df = reorder_columns(df, ['col2', 'col3'])
+
+    Method chaining example:
+
+    .. code-block:: python
+
+        import pandas as pd
+        import janitor
+        df = pd.DataFrame(...).reorder_columns(['col2', 'col3'])
+
+    The column order of `df` is now `col2`, `col3`, `col1`.
+
+    Internally, this function uses `DataFrame.reindex` with `copy=False`
+    to avoid unnecessary data duplication.
+
+    :param df: `DataFrame` to reorder
+    :param column_order: A list of column names or Pandas `Index`
+        specifying their order in the returned `DataFrame`.
+    :returns: A pandas DataFrame.
+    """
+
+    if not isinstance(column_order, (list, pd.Index)):
+        raise TypeError(
+            "column_order must be a list of column names or Pandas Index."
+        )
+
+    if any(col not in df.columns for col in column_order):
+        raise IndexError(
+            "A column in column_order was not found in the DataFrame."
+        )
+
+    # if column_order is a Pandas index, needs conversion to list:
+    column_order = list(column_order)
+
+    return df.reindex(
+        columns=(
+            column_order
+            + [col for col in df.columns if col not in column_order]
+        ),
+        copy=False,
+    )
+
+
 @pf.register_dataframe_method
 def coalesce(df, columns, new_column_name):
     """
@@ -459,16 +517,12 @@ def fill_empty(df, columns, value):
         for col in columns:
             assert (
                 col in df.columns
-            ), "{col} missing from dataframe columns!".format(
-                col=col
-            )
+            ), "{col} missing from dataframe columns!".format(col=col)
             df[col] = df[col].fillna(value)
     else:
         assert (
             columns in df.columns
-        ), "{col} missing from dataframe columns!".format(
-            col=columns
-        )
+        ), "{col} missing from dataframe columns!".format(col=columns)
         df[columns] = df[columns].fillna(value)
 
     return df


Test patch:
diff --git a/tests/test_functions.py b/tests/test_functions.py
--- a/tests/test_functions.py
+++ b/tests/test_functions.py
@@ -179,6 +179,33 @@ def test_rename_column(dataframe):
     )  # noqa: E501
 
 
+def test_reorder_columns(dataframe):
+    # sanity checking of inputs
+
+    # input is not a list or pd.Index
+    with pytest.raises(TypeError):
+        dataframe.reorder_columns("a")
+
+    # one of the columns is not present in the DataFrame
+    with pytest.raises(IndexError):
+        dataframe.reorder_columns(["notpresent"])
+
+    # reordering functionality
+
+    # sanity check when desired order matches current order
+    # this also tests whether the function can take Pandas Index objects
+    assert all(
+        dataframe.reorder_columns(dataframe.columns).columns
+        == dataframe.columns
+    )
+
+    # when columns are list & not all columns of DataFrame are included
+    assert all(
+        dataframe.reorder_columns(["animals", "Bell__Chart"]).columns
+        == ["animals", "Bell__Chart", "a", "decorated-elephant", "cities"]
+    )
+
+
 def test_coalesce():
     df = pd.DataFrame(
         {"a": [1, np.nan, 3], "b": [2, 3, 1], "c": [2, np.nan, 9]}