Repository: pyjanitor-devs/pyjanitor

Issue:
[ENH] deconcatenate_column to preserve the original column location
# Brief Description

<!-- Please provide a brief description of what you'd like to propose. -->
I would like to propose an enhancement to `deconcatenate_column` for preserving the original column position.

The current implementation will append the deconcatenated columns to the right of the original dataframe. In this way, the relative positions of the columns are not preserved.

I'd like to have a boolean kwarg that allows users to preserve the original column order.

# Example API
```python
df = (pd.DataFrame(...).
      deconcatenate_column(..., preserve_position=True)
```



Discussion:
Also, the examples in documentation uses plural, even though the function itself uses singular form (`deconcatenate_column`):
```python
df = deconcatenate_columns(df,
                           column_name='id',
                           new_column_names=['col1', 'col2'],
                           sep='-')
```
I can work on the enhancement and documents correction once I am done with the example notebook.
Thanks @shandou for picking out the doc inconsistency, and for proposing the new kwarg! Yes, definitely this would be a good PR to make. I will mark this as taken.

As a heads-up, more verbose docstrings on what `preserve_position` does, including a mini-example of some kind, would be very helpful. I would not skimp on text here; oftentimes there’s more lines of docstrings than implementation :smile:

Finally, don’t forget a test!

Resolving patch:
diff --git a/janitor/functions.py b/janitor/functions.py
--- a/janitor/functions.py
+++ b/janitor/functions.py
@@ -908,8 +908,9 @@ def concatenate_columns(
 def deconcatenate_column(
     df: pd.DataFrame,
     column_name,
-    new_column_names: Union[str, Iterable[str], Any],
+    new_column_names: Union[List[str], Tuple[str]],
     sep: str,
+    preserve_position: bool = False,
 ) -> pd.DataFrame:
     """
     De-concatenates a single column into multiple columns.
@@ -918,41 +919,72 @@ def deconcatenate_column(
 
     Used to quickly split columns out of a single column.
 
+    The keyword argument `preserve_position` takes `True` or `False` boolean
+    that controls whether the `new_column_names` will take the original
+    position of the to-be-deconcatenated `column_name`:
+
+    - When `preserve_position=False` (default), `df.columns` change from
+      `[..., column_name, ...]` to `[..., column_name, ..., new_column_names]`.
+      In other words, the deconcatenated new columns are appended to the right
+      of the original dataframe and the original `column_name` is NOT dropped.
+    - When `preserve_position=True`, `df.column` change from
+      `[..., column_name, ...]` to `[..., new_column_names, ...]`.
+      In other words, the deconcatenated new column will REPLACE the original
+      `column_name` at its original position, and `column_name` itself
+      is dropped.
+
     This method does not mutate the original DataFrame.
 
     Functional usage example:
 
     .. code-block:: python
 
-        df = deconcatenate_columns(df,
-                                   column_name='id',
-                                   new_column_names=['col1', 'col2'],
-                                   sep='-')
+        df = deconcatenate_column(
+                df, column_name='id', new_column_names=['col1', 'col2'],
+                sep='-', preserve_position=True
+        )
 
     Method chaining example:
 
     .. code-block:: python
 
         df = (pd.DataFrame(...).
-              deconcatenate_columns(column_name='id',
-                                    new_column_names=['col1', 'col2'],
-                                    sep='-'))
+                deconcatenate_column(
+                    column_name='id', new_column_names=['col1', 'col2'],
+                    sep='-', preserve_position=True
+                ))
 
     :param df: A pandas DataFrame.
     :param column_name: The column to split.
     :param new_column_names: A list of new column names post-splitting.
     :param sep: The separator delimiting the column's data.
+    :param preserve_position: Boolean for whether or not to preserve original
+        position of the column upon de-concatenation, default to False
     :returns: A pandas DataFrame with a deconcatenated column.
     """
     assert (
         column_name in df.columns
     ), f"column name {column_name} not present in dataframe"  # noqa: E501
     deconcat = df[column_name].str.split(sep, expand=True)
+    if preserve_position:
+        # Keep a copy of the original dataframe
+        df_original = df.copy()
     assert (
         len(new_column_names) == deconcat.shape[1]
     ), "number of new column names not correct."
     deconcat.columns = new_column_names
-    return df.join(deconcat)
+    df = pd.concat([df, deconcat], axis=1)
+    if preserve_position:
+        cols = list(df_original.columns)
+        index_original = cols.index(column_name)
+        for i, col_new in enumerate(new_column_names):
+            cols.insert(index_original + i, col_new)
+        df = df[cols].drop(columns=column_name)
+        assert (
+            len(df.columns)
+            == len(df_original.columns) + len(new_column_names) - 1
+        ), "number of columns after deconcatenation is incorrect"
+    return df
 
 
 @pf.register_dataframe_method


Test patch:
diff --git a/tests/functions/test_deconcatenate_column.py b/tests/functions/test_deconcatenate_column.py
--- a/tests/functions/test_deconcatenate_column.py
+++ b/tests/functions/test_deconcatenate_column.py
@@ -3,12 +3,12 @@
 
 @pytest.mark.functions
 def test_deconcatenate_column(dataframe):
-    df = dataframe.concatenate_columns(
+    df_orig = dataframe.concatenate_columns(
         column_names=["a", "decorated-elephant"],
         sep="-",
         new_column_name="index",
     )
-    df = df.deconcatenate_column(
+    df = df_orig.deconcatenate_column(
         column_name="index", new_column_names=["A", "B"], sep="-"
     )
     assert "A" in df.columns
diff --git a/tests/functions/test_deconcatenate_column_preserve_position.py b/tests/functions/test_deconcatenate_column_preserve_position.py
new file mode 100644
--- /dev/null
+++ b/tests/functions/test_deconcatenate_column_preserve_position.py
@@ -0,0 +1,29 @@
+import pytest
+
+
+@pytest.mark.functions
+def test_deconcatenate_column_preserve_position(dataframe):
+    df_original = dataframe.concatenate_columns(
+        column_names=["a", "decorated-elephant"],
+        sep="-",
+        new_column_name="index",
+    )
+    index_original = list(df_original.columns).index("index")
+    df = df_original.deconcatenate_column(
+        column_name="index",
+        new_column_names=["col1", "col2"],
+        sep="-",
+        preserve_position=True,
+    )
+    assert "index" not in df.columns, "column_name not dropped"
+    assert "col1" in df.columns, "new column not present"
+    assert "col2" in df.columns, "new column not present"
+    assert len(df_original.columns) + 1 == len(
+        df.columns
+    ), "Number of columns inconsistent"
+    assert (
+        list(df.columns).index("col1") == index_original
+    ), "Position not preserved"
+    assert (
+        list(df.columns).index("col2") == index_original + 1
+    ), "Position not preserved"