Repository: pyjanitor-devs/pyjanitor

Issue:
Rearrange order of columns with `names_sort`
# Brief Description

`names_sort` does not really sort anything. Either create a categorical, or use sort_index within the function to sort
# System Information
- Operating system: Linux
- Python version (required): 3.9.7

# Minimally Reproducible Code

<!-- If you provide minimal code that reproduces the problem, this makes it easier for us to debug what's going on.

Minimal code should be trivially copy/pastable into a Python interpreter in its entirety. Be sure to include imports.
-->

```py

| id | variable  | value |
|----|-----------|-------|
| 1  | outcome_1 | NaN   |
| 2  | outcome_1 | 18:33 |
| 2  | outcome_1 | 20:39 |
| 2  | outcome_3 | 01:40 |
| 3  | outcome_2 | 03:59 |
| 3  | outcome_4 | 07:46 |
| 3  | outcome_3 | 10:53 |

# output should be as below
# where the outcome columns are sorted 
| id_nmbr | outcome_1_0 | outcome_1_1 | outcome_2_0 | outcome_3_0 | outcome_4_0 |
|---------|-------------|-------------|-------------|-------------|-------------|
| 1       | NaN         | NaN         | NaN         | NaN         | NaN         |
| 2       | 18:33       | 20:39       | NaN         | 01:40       | NaN         |
| 3       | NaN         | NaN         | 03:59       | 07:46       | 10:53       |


# however, with `pivot_wider`, this is how it appears:
(df.assign(counter = df.groupby(group).cumcount())
   .pivot_wider(index='id', 
                names_from=['variable', 'counter'], 
                values_from='value')
) 
   id outcome_1_0 outcome_1_1 outcome_3_0 outcome_2_0 outcome_4_0
0   1         NaN         NaN         NaN         NaN         NaN
1   2       18:33       20:39       01:40         NaN         NaN
2   3         NaN         NaN       10:53       03:59       07:46

# passing a list of levels to `names_sort` would sort the data, 
# by passing it to `sort_index`. 
# For more control, 
# the user can ensure the columns are categorical columns before pivoting, 
# since implicitly, Pandas sorts the index when stacking/unstacking.
```


Discussion:

Resolving patch:
diff --git a/janitor/functions.py b/janitor/functions.py
--- a/janitor/functions.py
+++ b/janitor/functions.py
@@ -46,7 +46,6 @@
     _computations_pivot_wider,
     _currency_column_to_numeric,
     _data_checks_pivot_longer,
-    _data_checks_pivot_wider,
     _replace_empty_string_with_none,
     _replace_original_empty_string_with_none,
     _select_columns,
@@ -5928,7 +5927,7 @@ def pivot_longer(
         ignore_index,
     )
 
-    df = _computations_pivot_longer(
+    return _computations_pivot_longer(
         df,
         index,
         column_names,
@@ -5941,8 +5940,6 @@ def pivot_longer(
         ignore_index,
     )
 
-    return df
-
 
 @pf.register_dataframe_method
 def pivot_wider(
@@ -5950,7 +5947,6 @@ def pivot_wider(
     index: Optional[Union[List, str]] = None,
     names_from: Optional[Union[List, str]] = None,
     values_from: Optional[Union[List, str]] = None,
-    names_sort: Optional[bool] = False,
     levels_order: Optional[list] = None,
     flatten_levels: Optional[bool] = True,
     names_sep="_",
@@ -5969,168 +5965,6 @@ def pivot_wider(
         and `values_from` is possible using the
         `janitor.select_columns` syntax.
 
-    Reshaping to wide form :
-
-    .. code-block:: python
-
-             name variable  value
-        0   Alice      wk1      5
-        1   Alice      wk2      9
-        2   Alice      wk3     20
-        3   Alice      wk4     22
-        4     Bob      wk1      7
-        5     Bob      wk2     11
-        6     Bob      wk3     17
-        7     Bob      wk4     33
-        8   Carla      wk1      6
-        9   Carla      wk2     13
-        10  Carla      wk3     39
-        11  Carla      wk4     40
-
-        df.pivot_wider(
-                index = "name",
-                names_from = "variable",
-                values_from = "value"
-            )
-
-             name    wk1   wk2   wk3   wk4
-        0    Alice     5     9    20    22
-        1    Bob       7    11    17    33
-        2    Carla     6    13    39    40
-
-    Pivoting on multiple columns is possible :
-
-    .. code-block:: python
-
-            name    n  pct
-        0     1  10.0  0.1
-        1     2  20.0  0.2
-        2     3  30.0  0.3
-
-        (df.assign(num = 0)
-           .pivot_wider(
-              index = "num",
-              names_from = "name",
-              values_from = ["n", "pct"],
-              names_sep = "_"
-              )
-        )
-
-            num n_1  n_2  n_3  pct_1  pct_2  pct_3
-        0   0   10   20   30   0.1    0.2    0.3
-
-
-    You may choose not to flatten the columns,
-    by setting `flatten_levels` to False::
-
-        df
-
-           dependent_variable  step   a   b
-        0                 5.5     1  20  30
-        1                 5.5     2  25  37
-        2                 6.1     1  22  19
-        3                 6.1     2  18  29
-
-
-        df.pivot_wider(
-            index = "dep*",
-            names_from  = 'step',
-            flatten_levels = False
-            )
-
-                             a       b
-        step                 1   2   1   2
-        dependent_variable
-        5.5                 20  25  30  37
-        6.1                 22  18  19  29
-
-
-    The order of the levels can be changed with the `levels_order`
-    parameter, which internally uses `pd.DataFrame.reorder_levels`::
-
-        df.pivot_wider(
-            index = "dep*",
-            names_from  = 'step',
-            flatten_levels = False,
-            levels_order = ['step', None]
-            )
-
-        step                 1   2   1   2
-                             a   a   b   b
-        dependent_variable
-        5.5                 20  25  30  37
-        6.1                 22  18  19  29
-
-        df.pivot_wider(
-            index = ['a', 'b'],
-            names_from = 'name',
-            flatten_levels = True,
-            )
-
-           dependent_variable  a_1  a_2  b_1  b_2
-        0                 5.5   20   25   30   37
-        1                 6.1   22   18   19   29
-
-
-        df.pivot_wider(
-            index = ['a', 'b'],
-            names_from = 'name',
-            flatten_levels= True,
-            levels_order = ['step', None]
-            )
-
-           dependent_variable  1_a  2_a  1_b  2_b
-        0                 5.5   20   25   30   37
-        1                 6.1   22   18   19   29
-
-    `names_sep` and `names_glue` come in handy in situations where `names_from`
-    and/or `values_from` contain multiple variables; it is used primarily when
-    the columns are flattened. The default value for `names_sep` is `_`::
-
-        # default value of names_sep is '_'
-        df.pivot_wider(index = "dep*", names_from = "step")
-
-           dependent_variable  a_1  a_2  b_1  b_2
-        0                 5.5   20   25   30   37
-        1                 6.1   22   18   19   29
-
-        df.pivot_wider(
-            index = "dep*",
-            names_from = "step",
-            names_sep = "")
-
-           dependent_variable  a1  a2  b1  b2
-        0                 5.5  20  25  30  37
-        1                 6.1  22  18  19  29
-
-    With `names_glue` you can `glue` the individual levels (if MultiIndex)
-    into one (similar to `names_sep`), or you can modify the final columns,
-    as long as it can be passed to `pd.Index.map`::
-
-        # replicate `names_sep`
-        df.pivot_wider(
-            index = "dep*",
-            names_from = "step",
-            names_sep = None,
-            names_glue = "_".join
-            )
-
-           dependent_variable  a_1  a_2  b_1  b_2
-        0                 5.5   20   25   30   37
-        1                 6.1   22   18   19   29
-
-        # going beyond names_sep
-        df.pivot_wider(
-            index = "dep*",
-            names_from = "step",
-            names_sep = None,
-            names_glue = lambda col: f"{col[0]}_step{col[1]}"
-            )
-
-           dependent_variable  a_step1  a_step2  b_step1  b_step2
-        0                 5.5       20       25       30       37
-        1                 6.1       22       18       19       29
-
     .. note:: A ValueError is raised if the combination
         of the `index` and `names_from` is not unique.
 
@@ -6153,7 +5987,6 @@ def pivot_wider(
             index = [column1, column2, ...],
             names_from = [column3, column4, ...],
             value_from = [column5, column6, ...],
-            names_sort = True/False,
             levels_order = None/list,
             flatten_levels = True/False,
             names_sep='_',
@@ -6171,7 +6004,6 @@ def pivot_wider(
                 index = [column1, column2, ...],
                 names_from = [column3, column4, ...],
                 value_from = [column5, column6, ...],
-                names_sort = True/False,
                 levels_order = None/list,
                 flatten_levels = True/False,
                 names_sep='_',
@@ -6199,8 +6031,6 @@ def pivot_wider(
         will be used. Note that values from `values_from` are usually at
         the top level, the dataframe's columns is not flattened, or the
         start of each label in the columns, if flattened.
-    :param names_sort: Default is `True`. Sorts columns by order of
-        appearance.
     :param levels_order: Applicable if there are multiple `names_from`
         and/or `values_from`. Reorders the levels. Accepts a list of strings.
         If there are multiple `values_from`, pass a None to represent that
@@ -6217,50 +6047,21 @@ def pivot_wider(
         acceptable to pandas' `map` function.
     :returns: A pandas DataFrame that has been unpivoted from long to wide
         form.
-    :raises ValueError: if `names_from` is None.
-    :raises TypeError: if `flatten_levels` is not a boolean.
-
-    .. # noqa: DAR402
     """
 
     df = df.copy()
 
-    (
+    return _computations_pivot_wider(
         df,
         index,
         names_from,
         values_from,
-        names_sort,
-        levels_order,
-        flatten_levels,
-        names_sep,
-        names_glue,
-    ) = _data_checks_pivot_wider(
-        df,
-        index,
-        names_from,
-        values_from,
-        names_sort,
-        levels_order,
-        flatten_levels,
-        names_sep,
-        names_glue,
-    )
-
-    df = _computations_pivot_wider(
-        df,
-        index,
-        names_from,
-        values_from,
-        names_sort,
         levels_order,
         flatten_levels,
         names_sep,
         names_glue,
     )
 
-    return df
-
 
 @pf.register_dataframe_method
 def conditional_join(
diff --git a/janitor/utils.py b/janitor/utils.py
--- a/janitor/utils.py
+++ b/janitor/utils.py
@@ -28,7 +28,6 @@
 from pandas.core.reshape.merge import _MergeOperation
 from pandas.core.construction import extract_array
 from pandas.api.types import (
-    CategoricalDtype,
     is_extension_array_dtype,
     is_list_like,
     is_scalar,
@@ -1658,7 +1657,6 @@ def _data_checks_pivot_wider(
     index,
     names_from,
     values_from,
-    names_sort,
     levels_order,
     flatten_levels,
     names_sep,
@@ -1694,8 +1692,6 @@ def _data_checks_pivot_wider(
         if len(values_from) == 1:
             values_from = values_from[0]
 
-    check("names_sort", names_sort, [bool])
-
     if levels_order is not None:
         check("levesl_order", levels_order, [list])
 
@@ -1712,7 +1708,6 @@ def _data_checks_pivot_wider(
         index,
         names_from,
         values_from,
-        names_sort,
         levels_order,
         flatten_levels,
         names_sep,
@@ -1725,7 +1720,6 @@ def _computations_pivot_wider(
     index: Optional[Union[List, str]] = None,
     names_from: Optional[Union[List, str]] = None,
     values_from: Optional[Union[List, str]] = None,
-    names_sort: Optional[bool] = False,
     levels_order: Optional[list] = None,
     flatten_levels: Optional[bool] = True,
     names_sep="_",
@@ -1741,19 +1735,33 @@ def _computations_pivot_wider(
 
     A dataframe pivoted from long to wide form is returned.
     """
+
+    (
+        df,
+        index,
+        names_from,
+        values_from,
+        levels_order,
+        flatten_levels,
+        names_sep,
+        names_glue,
+    ) = _data_checks_pivot_wider(
+        df,
+        index,
+        names_from,
+        values_from,
+        levels_order,
+        flatten_levels,
+        names_sep,
+        names_glue,
+    )
     # check dtype of `names_from` is string
-    names_from_all_strings = df.filter(names_from).agg(is_string_dtype).all()
-
-    if names_sort:
-        # Categorical dtypes created only for `names_from`
-        # since that is what will become the new column names
-        dtypes = {
-            column_name: CategoricalDtype(
-                df[column_name].factorize(sort=False)[-1], ordered=True
-            )
-            for column_name in names_from
-        }
-        df = df.astype(dtypes)
+    names_from_all_strings = (
+        df.filter(names_from).agg(is_string_dtype).all().item()
+    )
+
+    # check dtype of columns
+    column_dtype = is_string_dtype(df.columns)
 
     df = df.pivot(  # noqa: PD010
         index=index, columns=names_from, values=values_from
@@ -1768,7 +1776,7 @@ def _computations_pivot_wider(
         return df
 
     # ensure all entries in names_from are strings
-    if not names_from_all_strings:
+    if (names_from_all_strings is False) or (column_dtype is False):
         if isinstance(df.columns, pd.MultiIndex):
             new_columns = [tuple(map(str, ent)) for ent in df]
             df.columns = pd.MultiIndex.from_tuples(new_columns)
@@ -1784,8 +1792,7 @@ def _computations_pivot_wider(
     # if columns are of category type
     # this returns columns to object dtype
     # also, resetting index with category columns is not possible
-    if names_sort:
-        df.columns = [*df.columns]
+    df.columns = [*df.columns]
 
     if index:
         df = df.reset_index()


Test patch:
diff --git a/tests/functions/test_pivot_wider.py b/tests/functions/test_pivot_wider.py
--- a/tests/functions/test_pivot_wider.py
+++ b/tests/functions/test_pivot_wider.py
@@ -300,8 +300,8 @@ def test_no_index():
     assert_frame_equal(result, expected_output)
 
 
-def test_no_index_names_sort_True():
-    """Test output if no `index` is supplied and `names_sort` is True."""
+def test_no_index_names_from_order():
+    """Test output if no `index` is supplied and column order is maintained."""
     df_in = pd.DataFrame(
         {
             "gender": ["Male", "Female", "Female", "Male", "Male"],
@@ -317,13 +317,15 @@ def test_no_index_names_sort_True():
         }
     )
 
-    result = df_in.pivot_wider(names_from="gender", names_sort=True)
+    result = df_in.encode_categorical(gender=(None, "appearance")).pivot_wider(
+        names_from="gender"
+    )
 
     assert_frame_equal(result, expected_output)
 
 
-def test_index_names_sort_True():
-    """Test output if index is supplied and `names_sort ` is True."""
+def test_index_names():
+    """Test output if index is supplied."""
     df = pd.DataFrame(
         [
             {"stat": "mean", "score": 4, "var": "var1"},
@@ -340,7 +342,7 @@ def test_index_names_sort_True():
     )
 
     result = df.pivot_wider(
-        index="var", names_from="stat", values_from="score", names_sort=True
+        index="var", names_from="stat", values_from="score"
     )
 
     assert_frame_equal(result, expected_output)
@@ -373,7 +375,10 @@ def test_names_glue():
 
 
 def test_change_level_order():
-    """Test output with `levels_order`"""
+    """
+    Test output with `levels_order`,
+    while maintaining order from `names_from`.
+    """
     df_in = pd.DataFrame(
         {
             "geoid": [1, 1, 13, 13],
@@ -399,12 +404,13 @@ def test_change_level_order():
         }
     )
 
-    result = df_in.pivot_wider(
+    result = df_in.encode_categorical(
+        variable=(None, "appearance")
+    ).pivot_wider(
         index=["geoid", "name"],
         names_from="variable",
         values_from=["estimate", "error"],
         levels_order=["variable", None],
-        names_sort=True,
     )
     assert_frame_equal(result, df_out)