Repository: pyjanitor-devs/pyjanitor

Issue:
[ENH] Allow `pivot_longer` to support multiple `.value`
# Brief Description

Allowing multiple `.value` could make some transformations simpler

# Example API

inspiration from [here](https://github.com/tidyverse/tidyr/issues/619#issuecomment-725530922)
```python
   x_1_mean  x_2_mean  x_1_sd  x_2_sd  y_1_mean  y_2_mean  y_1_sd  y_2_sd  unit
0         1         1       0   0.739         1         1       0  -0.525     1
1         2         1       1   0.219         2         1       1   0.623     2
2         3         0       1   1.460         3         0       1  -0.705     3
3         4         0       1   0.918         4         0       1   0.662     4

df.pivot_longer(index = 'unit', 
                names_to = (".value","time", ".value"), 
                names_pattern = r"(x|y)_([0-9])(_mean|_sd)")

   unit  time  x_mean   x_sd  y_mean   y_sd
0     1     1       1  0.000       1  0.000
1     1     2       1  0.739       1 -0.525
2     2     1       2  1.000       2  1.000
3     2     2       1  0.219       1  0.623
4     3     1       3  1.000       3  1.000
5     3     2       0  1.460       0 -0.705
6     4     1       4  1.000       4  1.000
7     4     2       0  0.918       0  0.662

# current approach involves a combination of pivot_longer and pivot_wider
(df
.pivot_longer('unit', names_to = ('letter', 'time', '.value'), names_sep = '_')
.pivot_wider(index = ['unit', 'time'], names_from = 'letter', levels_order = ['letter', None])
)
```



Discussion:

Resolving patch:
diff --git a/janitor/functions/pivot.py b/janitor/functions/pivot.py
--- a/janitor/functions/pivot.py
+++ b/janitor/functions/pivot.py
@@ -1,14 +1,13 @@
+import re
 from typing import Callable, List, Optional, Pattern, Tuple, Union
-import pandas_flavor as pf
-import pandas as pd
 
-from pandas.api.types import is_list_like, is_string_dtype
+import numpy as np
+import pandas as pd
+import pandas_flavor as pf
+from pandas.api.types import is_list_like, is_re_compilable, is_string_dtype
 
-from janitor.utils import check
 from janitor.functions.utils import _select_column_names
-import re
-import numpy as np
-from collections import defaultdict
+from janitor.utils import check
 
 
 @pf.register_dataframe_method
@@ -110,6 +109,31 @@ def pivot_longer(
         0   1        sp      m  5564      2
         1   1       rel      f    65      3
 
+    Use multiple `.value` to reshape dataframe:
+
+        >>> df = pd.DataFrame(
+        ...     [
+        ...         {
+        ...             "x_1_mean": 10,
+        ...             "x_2_mean": 20,
+        ...             "y_1_mean": 30,
+        ...             "y_2_mean": 40,
+        ...             "unit": 50,
+        ...         }
+        ...     ]
+        ... )
+        >>> df
+           x_1_mean  x_2_mean  y_1_mean  y_2_mean  unit
+        0        10        20        30        40    50
+        >>> df.pivot_longer(
+        ...     index="unit",
+        ...     names_to=(".value", "time", ".value"),
+        ...     names_pattern=r"(x|y)_([0-9])(_mean)",
+        ... )
+           unit time  x_mean  y_mean
+        0    50    1      10      30
+        1    50    2      20      40
+
 
 
     :param df: A pandas DataFrame.
@@ -128,6 +152,12 @@ def pivot_longer(
         names, if `name_sep` or `names_pattern` is provided.
         If `.value` is in `names_to`, new column names will be extracted
         from part of the existing column names and overrides`values_to`.
+    :param values_to: Name of new column as a string that will contain what
+        were previously the values of the columns in `column_names`.
+    :param column_level: If columns are a MultiIndex, then use this level to
+        unpivot the DataFrame. Provided for compatibility with pandas' melt,
+        and applies only if neither `names_sep` nor `names_pattern` is
+        provided.
     :param names_sep: Determines how the column name is broken up, if
         `names_to` contains multiple values. It takes the same
         specification as pandas' `str.split` method, and can be a string
@@ -142,12 +172,6 @@ def pivot_longer(
         `names_to` must also be a list/tuple and the lengths of both
         arguments must match.
         `names_pattern` does not work with MultiIndex columns.
-    :param values_to: Name of new column as a string that will contain what
-        were previously the values of the columns in `column_names`.
-    :param column_level: If columns are a MultiIndex, then use this level to
-        unpivot the DataFrame. Provided for compatibility with pandas' melt,
-        and applies only if neither `names_sep` nor `names_pattern` is
-        provided.
     :param sort_by_appearance: Default `False`. Boolean value that determines
         the final look of the DataFrame. If `True`, the unpivoted DataFrame
         will be stacked in order of first appearance.
@@ -201,144 +225,6 @@ def pivot_longer(
     )
 
 
-@pf.register_dataframe_method
-def pivot_wider(
-    df: pd.DataFrame,
-    index: Optional[Union[List, str]] = None,
-    names_from: Optional[Union[List, str]] = None,
-    values_from: Optional[Union[List, str]] = None,
-    levels_order: Optional[list] = None,
-    flatten_levels: Optional[bool] = True,
-    names_sep="_",
-    names_glue: Callable = None,
-) -> pd.DataFrame:
-    """
-    Reshapes data from *long* to *wide* form.
-
-    The number of columns are increased, while decreasing
-    the number of rows. It is the inverse of the
-    [`pivot_longer`][janitor.functions.pivot.pivot_longer]
-    method, and is a wrapper around `pd.DataFrame.pivot` method.
-
-    This method does not mutate the original DataFrame.
-
-    Column selection in `index`, `names_from` and `values_from`
-    is possible using the
-    [`select_columns`][janitor.functions.select_columns.select_columns] syntax.
-
-    A ValueError is raised if the combination
-    of the `index` and `names_from` is not unique.
-
-    By default, values from `values_from` are always
-    at the top level if the columns are not flattened.
-    If flattened, the values from `values_from` are usually
-    at the start of each label in the columns.
-
-
-    Example:
-
-        >>> import pandas as pd
-        >>> import janitor
-        >>> df = [{'dep': 5.5, 'step': 1, 'a': 20, 'b': 30},
-        ...       {'dep': 5.5, 'step': 2, 'a': 25, 'b': 37},
-        ...       {'dep': 6.1, 'step': 1, 'a': 22, 'b': 19},
-        ...       {'dep': 6.1, 'step': 2, 'a': 18, 'b': 29}]
-        >>> df = pd.DataFrame(df)
-        >>> df
-           dep  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
-
-    Pivot and flatten columns:
-
-        >>> df.pivot_wider(
-        ...     index = "dep",
-        ...     names_from = "step",
-        ... )
-           dep  a_1  a_2  b_1  b_2
-        0  5.5   20   25   30   37
-        1  6.1   22   18   19   29
-
-    Change the order of the column labels:
-
-        >>> df.pivot_wider(
-        ...     index = "dep",
-        ...     names_from = "step",
-        ...     levels_order = ["step", None],
-        ... )
-           dep  1_a  2_a  1_b  2_b
-        0  5.5   20   25   30   37
-        1  6.1   22   18   19   29
-
-    Modify columns with `names_sep`:
-
-        >>> df.pivot_wider(
-        ...     index = "dep",
-        ...     names_from = "step",
-        ...     names_sep = "",
-        ... )
-           dep  a1  a2  b1  b2
-        0  5.5  20  25  30  37
-        1  6.1  22  18  19  29
-
-    Modify columns with `names_glue`:
-
-        >>> df.pivot_wider(
-        ...     index = "dep",
-        ...     names_from = "step",
-        ...     names_sep = None,
-        ...     names_glue = lambda col: f"{col[0]}_step{col[1]}",
-        ... )
-           dep  a_step1  a_step2  b_step1  b_step2
-        0  5.5       20       25       30       37
-        1  6.1       22       18       19       29
-
-
-    :param df: A pandas DataFrame.
-    :param index: Name(s) of columns to use as identifier variables.
-        It should be either a single column name, or a list of column names.
-        If `index` is not provided, the DataFrame's index is used.
-    :param names_from: Name(s) of column(s) to use to make the new
-        DataFrame's columns. Should be either a single column name,
-        or a list of column names.
-    :param values_from: Name(s) of column(s) that will be used for populating
-        the new DataFrame's values.
-        If `values_from` is not specified,  all remaining columns
-        will be used.
-    :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 `None` to represent that
-        level.
-    :param flatten_levels: Default is `True`. If `False`, the DataFrame stays
-        as a MultiIndex.
-    :param names_sep: If `names_from` or `values_from` contain multiple
-        variables, this will be used to join the values into a single string
-        to use as a column name. Default is `_`.
-        Applicable only if `flatten_levels` is `True`.
-    :param names_glue: A callable to control the output
-        of the flattened columns.
-        Applicable only if `flatten_levels` is `True`.
-        Function should be acceptable to pandas’ `map` function.
-    :returns: A pandas DataFrame that has been unpivoted from long to wide
-        form.
-    """
-
-    df = df.copy()
-
-    return _computations_pivot_wider(
-        df,
-        index,
-        names_from,
-        values_from,
-        levels_order,
-        flatten_levels,
-        names_sep,
-        names_glue,
-    )
-
-
 def _data_checks_pivot_longer(
     df,
     index,
@@ -371,14 +257,14 @@ def _data_checks_pivot_longer(
 
     if index is not None:
         if is_list_like(index) and (not isinstance(index, tuple)):
-            index = list(index)
+            index = [*index]
         index = _select_column_names(index, df)
 
     if column_names is not None:
         if is_list_like(column_names) and (
             not isinstance(column_names, tuple)
         ):
-            column_names = list(column_names)
+            column_names = [*column_names]
         column_names = _select_column_names(column_names, df)
 
     len_names_to = 0
@@ -386,27 +272,19 @@ def _data_checks_pivot_longer(
         if isinstance(names_to, str):
             names_to = [names_to]
         elif isinstance(names_to, tuple):
-            names_to = list(names_to)
+            names_to = [*names_to]
         check("names_to", names_to, [list])
 
         unique_names_to = set()
         for word in names_to:
             if not isinstance(word, str):
                 raise TypeError(
-                    f"""
-                    All entries in the names_to
-                    argument must be strings.
-                    {word} is of type {type(word).__name__}
-                    """
+                    "All entries in the names_to argument must be strings. "
+                    f"{word} is of type {type(word).__name__}."
                 )
 
-            if word in unique_names_to:
-                raise ValueError(
-                    f"""
-                    {word} already exists in names_to.
-                    Duplicates are not allowed.
-                    """
-                )
+            if (word in unique_names_to) and (word != ".value"):
+                raise ValueError(f"{word} already exists in names_to.")
             unique_names_to.add(word)  # noqa: PD005
         unique_names_to = None
 
@@ -414,72 +292,53 @@ def _data_checks_pivot_longer(
 
     if names_sep and names_pattern:
         raise ValueError(
-            """
-                Only one of names_pattern or names_sep
-                should be provided.
-                """
+            "Only one of names_pattern or names_sep should be provided."
         )
 
     if names_pattern is not None:
         check("names_pattern", names_pattern, [str, Pattern, list, tuple])
         if names_to is None:
-            raise ValueError(
-                """
-                Kindly provide values for names_to.
-                """
-            )
+            raise ValueError("Kindly provide values for names_to.")
         if isinstance(names_pattern, (str, Pattern)):
             num_regex_grps = re.compile(names_pattern).groups
 
             if len_names_to != num_regex_grps:
                 raise ValueError(
-                    f"""
-                    The length of names_to does not match
-                    the number of groups in names_pattern.
-                    The length of names_to is {len_names_to}
-                    while the number of groups in the regex
-                    is {num_regex_grps}
-                    """
+                    f"The length of names_to does not match "
+                    "the number of groups in names_pattern. "
+                    f"The length of names_to is {len_names_to} "
+                    "while the number of groups in the regex "
+                    f"is {num_regex_grps}."
                 )
 
         if isinstance(names_pattern, (list, tuple)):
             for word in names_pattern:
-                if not isinstance(word, (str, Pattern)):
+                if not is_re_compilable(word):
                     raise TypeError(
-                        f"""
-                        All entries in the names_pattern argument
-                        must be regular expressions.
-                        `{word}` is of type {type(word).__name__}
-                        """
+                        f"All entries in the names_pattern argument "
+                        "must be regular expressions, "
+                        "or can be compiled into a regex. "
+                        f"`{word}` is of type {type(word).__name__}."
                     )
 
             if len(names_pattern) != len_names_to:
                 raise ValueError(
-                    f"""
-                    The length of names_to does not match
-                    the number of regexes in names_pattern.
-                    The length of names_to is {len_names_to}
-                    while the number of regexes
-                    is {len(names_pattern)}
-                    """
+                    f"The length of names_to does not match "
+                    "the number of regexes in names_pattern. "
+                    f"The length of names_to is {len_names_to} "
+                    f"while the number of regexes is {len(names_pattern)}."
                 )
 
             if names_to and (".value" in names_to):
                 raise ValueError(
-                    """
-                    `.value` is not accepted in names_to
-                    if names_pattern is a list/tuple.
-                    """
+                    ".value is not accepted in names_to "
+                    "if names_pattern is a list/tuple."
                 )
 
     if names_sep is not None:
         check("names_sep", names_sep, [str, Pattern])
         if names_to is None:
-            raise ValueError(
-                """
-                Kindly provide values for names_to.
-                """
-            )
+            raise ValueError("Kindly provide values for names_to.")
 
     check("values_to", values_to, [str])
     df_columns = df.columns
@@ -491,32 +350,25 @@ def _data_checks_pivot_longer(
         # copied from pandas' melt source code
         # with a minor tweak
         raise ValueError(
-            """
-            This dataframe has a column name that matches the
-            values_to argument.
-            Kindly set the values_to parameter to a unique name.
-            """
+            "This dataframe has a column name "
+            "that matches the values_to argument. "
+            "Kindly set the values_to parameter to a unique name."
         )
 
     # avoid duplicate columns in the final output
     if (names_to is not None) and (not dot_value) and (values_to in names_to):
         raise ValueError(
-            """
-            `values_to` is present in names_to;
-            this is not allowed. Kindly use a unique
-            name.
-            """
+            "The argument provided for values_to already exists in names_to; "
+            "Kindly use a unique name."
         )
 
     if any((names_sep, names_pattern)) and (
         isinstance(df_columns, pd.MultiIndex)
     ):
         raise ValueError(
-            """
-            Unpivoting a MultiIndex column dataframe
-            when names_sep or names_pattern is supplied
-            is not supported.
-            """
+            "Unpivoting a MultiIndex column dataframe "
+            "when names_sep or names_pattern is supplied "
+            "is not supported."
         )
 
     if all((names_sep is None, names_pattern is None)):
@@ -527,10 +379,8 @@ def _data_checks_pivot_longer(
             and (not isinstance(index, list))
         ):
             raise ValueError(
-                """
-                index must be a list of tuples
-                when columns are a MultiIndex.
-                """
+                "index must be a list of tuples "
+                "when the columns are a MultiIndex."
             )
 
         if (
@@ -539,10 +389,8 @@ def _data_checks_pivot_longer(
             and (not isinstance(column_names, list))
         ):
             raise ValueError(
-                """
-                column_names must be a list of tuples
-                when columns are a MultiIndex.
-                """
+                "column_names must be a list of tuples "
+                "when the columns are a MultiIndex."
             )
 
     check("sort_by_appearance", sort_by_appearance, [bool])
@@ -579,29 +427,24 @@ def _computations_pivot_longer(
     This is where the final dataframe in long form is created.
     """
 
-    if (
-        (index is None)
-        and column_names
-        and (df.columns.size > len(column_names))
-    ):
+    if (index is None) and column_names:
         index = [
             column_name
             for column_name in df
             if column_name not in column_names
         ]
+        if not index:
+            index = None
 
     # scenario 1
     if all((names_pattern is None, names_sep is None)):
-        if names_to:
+        if names_to and index:
             for word in names_to:
                 if word in index:
                     raise ValueError(
-                        f"""
-                        `{word}` in names_to already exists
-                        in column labels assigned
-                        to the dataframe's index parameter.
-                        Kindly use a unique name.
-                        """
+                        f"`{word}` in names_to already exists "
+                        "in column labels assigned to the dataframe's "
+                        "index parameter. Kindly use a unique name."
                     )
 
         len_index = len(df)
@@ -620,7 +463,7 @@ def _computations_pivot_longer(
             df = _sort_by_appearance_for_melt(df=df, len_index=len_index)
 
         if ignore_index:
-            df.index = np.arange(len(df))
+            df.index = range(len(df))
 
         return df
 
@@ -631,40 +474,21 @@ def _computations_pivot_longer(
     if column_names:
         df = df.loc[:, column_names]
 
-    df_index_names = df.index.names
+    # df_index_names = df.index.names
 
-    # checks to avoid duplicate columns
+    # check to avoid duplicate columns in the final dataframe
     # idea is that if there is no `.value`
     # then the word should not exist in the index
-    # if, however there is `.value`
-    # then the word should not be found in
-    # neither the index or column names
-
-    # idea from pd.wide_to_long
-    for word in names_to:
-        if (".value" not in names_to) and (word in df_index_names):
-            raise ValueError(
-                f"""
-                `{word}` in names_to already exists
-                in column labels assigned
-                to the dataframe's index.
-                Kindly use a unique name.
-                """
-            )
-
-        if (
-            (".value" in names_to)
-            and (word != ".value")
-            and (word in df_index_names)
-        ):
-            raise ValueError(
-                f"""
-                `{word}` in names_to already exists
-                in column labels assigned
-                to the dataframe's index.
-                Kindly use a unique name.
-                """
-            )
+    # there is no need to check the columns
+    # since that is what we are replacing with names_to
+    if ".value" not in names_to:
+        for word in names_to:
+            if index and (word in index):
+                raise ValueError(
+                    f"`{word}` in names_to already exists "
+                    "as a column label assigned to the dataframe's index. "
+                    "Kindly use a unique name."
+                )
 
     if names_sep:
         return _pivot_longer_names_sep(
@@ -693,123 +517,33 @@ def _computations_pivot_longer(
     )
 
 
-def _computations_pivot_wider(
-    df: pd.DataFrame,
-    index: Optional[Union[List, str]] = None,
-    names_from: Optional[Union[List, str]] = None,
-    values_from: Optional[Union[List, str]] = None,
-    levels_order: Optional[list] = None,
-    flatten_levels: Optional[bool] = True,
-    names_sep="_",
-    names_glue: Callable = None,
+def _sort_by_appearance_for_melt(
+    df: pd.DataFrame, len_index: int
 ) -> pd.DataFrame:
     """
-    This is the main workhorse of the `pivot_wider` function.
-
-    It is a wrapper around `pd.pivot`. For a MultiIndex, the
-    order of the levels can be changed with `levels_order`.
-    The output for multiple `names_from` and/or `values_from`
-    can be controlled with `names_glue` and/or `names_sep`.
+    This function sorts the resulting dataframe by appearance,
+    via the `sort_by_appearance` parameter in `computations_pivot_longer`.
 
-    A dataframe pivoted from long to wide form is returned.
+    A dataframe that is sorted by appearance 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().item()
-    )
+    # explanation here to help future me :)
 
-    # check dtype of columns
-    column_dtype = is_string_dtype(df.columns)
+    # if the height of the new dataframe
+    # is the same as the height of the original dataframe,
+    # then there is no need to sort by appearance
+    length_check = any((len_index == 1, len_index == len(df)))
 
-    df = df.pivot(  # noqa: PD010
-        index=index, columns=names_from, values=values_from
-    )
+    # pd.melt flips the columns into vertical positions
+    # it `tiles` the index during the flipping
+    # example:
 
-    if levels_order and (isinstance(df.columns, pd.MultiIndex)):
-        df = df.reorder_levels(order=levels_order, axis="columns")
+    #          first last  height  weight
+    # person A  John  Doe     5.5     130
+    #        B  Mary   Bo     6.0     150
 
-    # an empty df is likely because
-    # there is no `values_from`
-    if any((df.empty, flatten_levels is False)):
-        return df
-
-    # ensure all entries in names_from are 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)
-        else:
-            df.columns = df.columns.astype(str)
-
-    if (names_sep is not None) and (isinstance(df.columns, pd.MultiIndex)):
-        df.columns = df.columns.map(names_sep.join)
-
-    if names_glue:
-        df.columns = df.columns.map(names_glue)
-
-    # if columns are of category type
-    # this returns columns to object dtype
-    # also, resetting index with category columns is not possible
-    df.columns = [*df.columns]
-
-    if index:
-        df = df.reset_index()
-
-    if df.columns.names:
-        df = df.rename_axis(columns=None)
-
-    return df
-
-
-def _sort_by_appearance_for_melt(
-    df: pd.DataFrame, len_index: int
-) -> pd.DataFrame:
-    """
-    This function sorts the resulting dataframe by appearance,
-    via the `sort_by_appearance` parameter in `computations_pivot_longer`.
-
-    A dataframe that is sorted by appearance is returned.
-    """
-
-    index_sorter = None
-
-    # explanation here to help future me :)
-
-    # if the height of the new dataframe
-    # is the same as the height of the original dataframe,
-    # then there is no need to sort by appearance
-    length_check = any((len_index == 1, len_index == len(df)))
-
-    # pd.melt flips the columns into vertical positions
-    # it `tiles` the index during the flipping
-    # example:
-
-    #          first last  height  weight
-    # person A  John  Doe     5.5     130
-    #        B  Mary   Bo     6.0     150
-
-    # melting the dataframe above yields:
-    # df.melt(['first', 'last'])
+    # melting the dataframe above yields:
+    # df.melt(['first', 'last'])
 
     #   first last variable  value
     # 0  John  Doe   height    5.5
@@ -831,15 +565,15 @@ def _sort_by_appearance_for_melt(
     # get the lengths of the dataframe
     # before and after it is melted
     # for the example above, the length before melting is 2
-    # and after - 4.
+    # and after -> 4.
     # reshaping allows us to track the original positions
     # in the previous dataframe ->
-    # np.reshape([0,1,2,3], (-1, 2))
+    # np.reshape([0,1,2,3], (-1, 2)) becomes
     # array([[0, 1],
     #        [2, 3]])
     # ravel, with the Fortran order (`F`) ensures the John's are aligned
     # before the Mary's -> [0, 2, 1, 3]
-    # the raveled array is then passed to `take`
+    # the flattened array is then passed to `take`
     if not length_check:
         index_sorter = np.arange(len(df))
         index_sorter = np.reshape(index_sorter, (-1, len_index))
@@ -849,7 +583,7 @@ def _sort_by_appearance_for_melt(
     return df
 
 
-def _pivot_longer_frame_MultiIndex(
+def _pivot_longer_not_dot_value(
     df: pd.DataFrame,
     index,
     sort_by_appearance: bool,
@@ -857,242 +591,157 @@ def _pivot_longer_frame_MultiIndex(
     values_to: str,
 ) -> pd.DataFrame:
     """
-    This creates the final dataframe,
-    where names_sep/names_pattern is provided,
-    and the extraction/split of the columns
-    result in a MultiIndex. This applies only
-    to names_sep or names_pattern as a string,
-    where more than one group is present in the
-    regex.
+    Pivots the dataframe into the final form,
+    for scenarios where names_pattern is a string/regex,
+    or names_sep is provided, and .value is not in names_to.
+
+    Returns a DataFrame.
     """
 
     len_index = len(df)
-    mapping = df.columns
-    if ".value" not in mapping.names:
-        df = df.melt(ignore_index=False, value_name=values_to)
-
-        if sort_by_appearance:
-            df = _sort_by_appearance_for_melt(df=df, len_index=len_index)
-
-        if index:
-            df = df.reset_index(index)
-
-        if ignore_index:
-            df.index = range(len(df))
-
-        return df
-
-    # labels that are not `.value`
-    # required when recombining list of individual dataframes
-    # as they become the keys in the concatenation
-    others = mapping.droplevel(".value").unique()
-    if isinstance(others, pd.MultiIndex):
-        levels = others.names
-    else:
-        levels = others.name
-    # here, we get the dataframes containing the `.value` labels
-    # as columns
-    # and then concatenate vertically, using the other variables
-    # in `names_to`, which in this is case, is captured in `others`
-    # as keys. This forms a MultiIndex; reset_index puts it back
-    # as columns into the dataframe.
-    df = [df.xs(key=key, axis="columns", level=levels) for key in others]
-    df = pd.concat(df, keys=others, axis="index", copy=False, sort=False)
-    if isinstance(levels, str):
-        levels = [levels]
-    # represents the cumcount,
-    # used in making the columns unique (if they werent originally)
-    null_in_levels = None in levels
-    # gets rid of None, for scenarios where we
-    # generated cumcount to make the columns unique
-    levels = [level for level in levels if level]
-    # need to order the dataframe's index
-    # so that when resetting,
-    # the index appears before the other columns
-    # this is relevant only if `index` is True
-    # using numbers here, in case there are multiple Nones
-    # in the index names
-    if index:
-        new_order = np.roll(np.arange(len(df.index.names)), len(index) + 1)
-        df = df.reorder_levels(new_order, axis="index")
-        df = df.reset_index(level=index + levels)
-    else:
-        df = df.reset_index(levels)
-
-    if null_in_levels:
-        df = df.droplevel(level=-1, axis="index")
 
-    if df.columns.names:
-        df = df.rename_axis(columns=None)
+    df = df.melt(ignore_index=False, value_name=values_to)
 
     if sort_by_appearance:
         df = _sort_by_appearance_for_melt(df=df, len_index=len_index)
 
+    if index:
+        df = df.reset_index(index)
+
     if ignore_index:
         df.index = range(len(df))
 
+    if df.columns.names:
+        df.columns.names = [None]
+
     return df
 
 
-def _pivot_longer_frame_single_Index(
+def _pivot_longer_dot_value(
     df: pd.DataFrame,
-    index,
+    index: Union[list, None],
     sort_by_appearance: bool,
     ignore_index: bool,
-    values_to: str = None,
+    names_to: list,
+    mapping: pd.DataFrame,
 ) -> pd.DataFrame:
     """
-    This creates the final dataframe,
-    where names_pattern is provided,
-    and the extraction/split of the columns
-    result in a single Index.
-    This covers scenarios where names_pattern
-    is a list/tuple, or where a single group
-    is present in the regex string.
-    """
+    Pivots the dataframe into the final form,
+    for scenarios where names_pattern is a string/regex,
+    or names_sep is provided, and .value is in names_to.
 
-    if df.columns.name != ".value":
-        len_index = len(df)
-        df = df.melt(ignore_index=False, value_name=values_to)
-
-        if sort_by_appearance:
-            df = _sort_by_appearance_for_melt(df=df, len_index=len_index)
-
-        if index:
-            df = df.reset_index(index)
-
-        if ignore_index:
-            df.index = range(len(df))
-
-        return df
-
-    mapping = df.columns
-    len_df_columns = mapping.size
-    mapping = mapping.unique()
-    len_mapping = mapping.size
-
-    len_index = len(df)
-
-    if len_df_columns > 1:
-        container = defaultdict(list)
-        for name, series in df.items():
-            container[name].append(series)
-        if len_mapping == 1:  # single unique column
-            container = container[mapping[0]]
-            df = pd.concat(
-                container, axis="index", join="outer", sort=False, copy=False
+    Returns a DataFrame.
+    """
+    # shrink to a single .value, if multiple .value
+    if np.count_nonzero(mapping.columns == ".value") > 1:
+        _value = mapping.loc[:, ".value"].agg("".join, axis="columns")
+        mapping = mapping.drop(columns=".value").assign(**{".value": _value})
+    # check to avoid duplicate columns
+    # the names assigned to `.value` should not be found in the index
+    # and should not be found in the other names in names_to
+    exclude = mapping[".value"].array
+    for word in names_to:
+        if (word != ".value") and (word in exclude):
+            raise ValueError(
+                f"{word} in names_to already exists "
+                "in the new dataframe's columns. "
+                "Kindly use a unique name."
             )
-            df = df.to_frame()
-        else:
-            # concat works fine here and efficient too,
-            # since we are combining Series
-            # a Series is returned for each concatenation
-            # the outer keys serve as a pairing mechanism
-            # for recombining the dataframe
-            # so if we have a dataframe like below:
-            #        id  x1  x2  y1  y2
-            #    0   1   4   5   7  10
-            #    1   2   5   6   8  11
-            #    2   3   6   7   9  12
-            # then x1 will pair with y1, and x2 will pair with y2
-            # if the dataframe column positions were alternated, like below:
-            #        id  x2  x1  y1  y2
-            #    0   1   5   4   7  10
-            #    1   2   6   5   8  11
-            #    2   3   7   6   9  12
-            # then x2 will pair with y1 and x1 will pair with y2
-            # it is simply a first come first serve approach
-            df = [
-                pd.concat(value, copy=False, keys=np.arange(len(value)))
-                for _, value in container.items()
-            ]
-            first, *rest = df
-            first = first.to_frame()
-            df = first.join(rest, how="outer", sort=False)
-            # drop outermost keys (used in the concatenation)
-            df = df.droplevel(level=0, axis="index")
+    if index:
+        exclude = set(index).intersection(exclude)
+        if exclude:
+            raise ValueError(
+                f"Labels {*exclude, } already exist "
+                "as column labels assigned to the dataframe's "
+                "index parameter. Kindly use a unique name."
+            )
+    # reorder allows for easy column selection later
+    # in the concatenation phase
+    # basically push .value to the end,
+    # so we can easily do a .loc, to keep .value columns as headers
+    other = [entry for entry in names_to if entry != ".value"]
+    last = mapping.columns[-1]
+    if other and (last != ".value"):
+        mapping = mapping.move(
+            source=".value", target=last, position="after", axis=1
+        )
 
-    if df.columns.names:
-        df = df.rename_axis(columns=None)
+    # useful if order of columns change during sorting
+    # for a MultiIndex
+    _value = mapping[".value"].unique()
+    if sort_by_appearance & (len(mapping.columns) > 1):
+        mapping = mapping.encode_categorical(
+            **{col: "appearance" for col in names_to}
+        )
+    # having unique columns ensure the data can be recombined
+    # successfully via pd.concat; if the columns are not unique,
+    # a counter is created with cumcount to ensure uniqueness.
+    # This is dropped later on, and is not part of the final
+    # dataframe.
+    mapping_is_unique = not mapping.duplicated().any(axis=None).item()
+    if mapping_is_unique:
+        if len(mapping.columns) == 1:
+            mapping = mapping.iloc[:, 0]
+        else:
+            mapping = pd.MultiIndex.from_frame(mapping)
+    else:
+        mapping = [mapping.groupby(names_to, sort=False).cumcount()] + [
+            series for _, series in mapping.items()
+        ]
+        mapping = pd.MultiIndex.from_arrays(mapping)
+    df.columns = mapping
 
-    if sort_by_appearance:
-        df = _sort_by_appearance_for_melt(df=df, len_index=len_index)
+    len_index = len(df)
 
+    if len(df.columns.names) > 1:
+        # improve performance for MultiIndex selection
+        # during the list comprehension ---> [df.loc[:, n] for n in others]
+        # and avoid lexsort performance warning
+        if not df.columns.is_monotonic_increasing:
+            df = df.sort_index(axis="columns")
+        others = df.columns.droplevel(".value").unique()
+        df = [df.loc[:, n] for n in others]
+        df = pd.concat(df, keys=others, axis="index", copy=False, sort=False)
+        # intercept and reorder column if needed
+        if not df.columns.equals(_value):
+            df = df.reindex(columns=_value)
     if index:
-        df = df.reset_index(index)
+        num_levels = df.index.nlevels
+        # need to order the dataframe's index
+        # so that when resetting,
+        # the index appears before the other columns
+        # this is relevant only if `index` is True
+        # using numbers here, in case there are multiple Nones
+        # in the index names
+        if num_levels > 1:
+            new_order = np.roll(np.arange(num_levels), len(index) + 1)
+            df = df.reorder_levels(new_order, axis="index")
+            # if names_to contains variables other than `.value`
+            # restore them back to the dataframe as columns
+            if other:
+                index.extend(other)
+        df = df.reset_index(level=index)
+    else:
+        # if names_to contains variables other than `.value`
+        # restore them back to the dataframe as columns
+        if other:
+            df = df.reset_index(level=other)
+    if sort_by_appearance:
+        df = _sort_by_appearance_for_melt(df, len_index)
 
     if ignore_index:
         df.index = range(len(df))
+    else:
+        if None in mapping.names:
+            df = df.droplevel(0)
+    df.columns.names = [None]
 
     return df
 
 
-def _data_checks_pivot_wider(
-    df,
-    index,
-    names_from,
-    values_from,
-    levels_order,
-    flatten_levels,
-    names_sep,
-    names_glue,
-):
-
-    """
-    This function raises errors if the arguments have the wrong
-    python type, or if the column does not exist in the dataframe.
-    This function is executed before proceeding to the computation phase.
-    Type annotations are not provided because this function is where type
-    checking happens.
-    """
-
-    if index is not None:
-        if is_list_like(index):
-            index = [*index]
-        index = _select_column_names(index, df)
-
-    if names_from is None:
-        raise ValueError(
-            "pivot_wider() is missing 1 required argument: 'names_from'"
-        )
-
-    if is_list_like(names_from):
-        names_from = [*names_from]
-    names_from = _select_column_names(names_from, df)
-
-    if values_from is not None:
-        if is_list_like(values_from):
-            values_from = [*values_from]
-        values_from = _select_column_names(values_from, df)
-        if len(values_from) == 1:
-            values_from = values_from[0]
-
-    if levels_order is not None:
-        check("levesl_order", levels_order, [list])
-
-    check("flatten_levels", flatten_levels, [bool])
-
-    if names_sep is not None:
-        check("names_sep", names_sep, [str])
-
-    if names_glue is not None:
-        check("names_glue", names_glue, [callable])
-
-    return (
-        df,
-        index,
-        names_from,
-        values_from,
-        levels_order,
-        flatten_levels,
-        names_sep,
-        names_glue,
-    )
-
-
 def _pivot_longer_names_pattern_sequence(
     df: pd.DataFrame,
-    index,
+    index: Union[list, None],
     names_to: list,
     names_pattern: Union[list, tuple],
     sort_by_appearance: bool,
@@ -1104,53 +753,69 @@ def _pivot_longer_names_pattern_sequence(
     """
 
     df_columns = df.columns
+    len_index = len(df)
+
     mapping = [
         df_columns.str.contains(regex, na=False, regex=True)
         for regex in names_pattern
     ]
 
-    matches = [arr.any() for arr in mapping]
-    if np.any(matches).item() is False:
-        raise ValueError(
-            """
-            No label in the columns
-            matched the regexes
-            in names_pattern.
-            Kindly provide regexes
-            that match all labels
-            in the columns.
-            """
-        )
+    matches = (arr.any() for arr in mapping)
+    # within each match, check the individual matches
+    # and raise an error if any is False
     for position, boolean in enumerate(matches):
-        if boolean.item() is False:
+        if not boolean.item():
             raise ValueError(
-                f"""
-                No match was returned for
-                regex `{names_pattern[position]}`
-                """
+                "No match was returned for the regex "
+                f"at position {position} -> {names_pattern[position]}."
             )
 
     mapping = np.select(mapping, names_to, None)
-    # guard .. for scenarios where not all labels
-    # in the columns are matched to the regex(es)
-    # the any_nulls takes care of that,
-    # via boolean indexing
-    any_nulls = pd.notna(mapping)
-    mapping = pd.MultiIndex.from_arrays([mapping, df_columns])
-    mapping.names = [".value", None]
-    df.columns = mapping
-    if any_nulls.any():
-        df = df.loc[:, any_nulls]
-    df = df.droplevel(level=-1, axis="columns")
 
-    return _pivot_longer_frame_single_Index(
-        df, index, sort_by_appearance, ignore_index, values_to=None
-    )
+    # only matched columns are retained
+    matches = pd.notna(mapping)
+
+    df = df.loc[:, matches]
+    mapping = mapping[matches]
+    if len(mapping) == 1:
+        df.columns = mapping
+    else:
+        mapping = pd.Series(mapping)
+        # get positions of columns,
+        # to ensure interleaving is possible
+        # so if we have a dataframe like below:
+        #        id  x1  x2  y1  y2
+        #    0   1   4   5   7  10
+        #    1   2   5   6   8  11
+        #    2   3   6   7   9  12
+        # then x1 will pair with y1, and x2 will pair with y2
+        # if the dataframe column positions were alternated, like below:
+        #        id  x2  x1  y1  y2
+        #    0   1   5   4   7  10
+        #    1   2   6   5   8  11
+        #    2   3   7   6   9  12
+        # then x2 will pair with y1 and x1 will pair with y2
+        # it is simply a first come first serve approach
+        positions = mapping.groupby(mapping, sort=False).cumcount()
+        df.columns = [positions, mapping]
+        df = [df.loc[:, num] for num in positions.unique()]
+        df = pd.concat(df, axis="index", sort=False, copy=False)
+
+    if sort_by_appearance:
+        df = _sort_by_appearance_for_melt(df=df, len_index=len_index)
+
+    if index:
+        df = df.reset_index(index)
+
+    if ignore_index:
+        df.index = range(len(df))
+
+    return df
 
 
 def _pivot_longer_names_pattern_str(
     df: pd.DataFrame,
-    index,
+    index: Union[list, None],
     names_to: list,
     names_pattern: Union[str, Pattern],
     values_to: str,
@@ -1159,78 +824,48 @@ def _pivot_longer_names_pattern_str(
 ) -> pd.DataFrame:
     """
     This takes care of pivoting scenarios where
-    names_pattern is provided, and is a string.
+    names_pattern is provided, and is a string/regex.
     """
-
     mapping = df.columns.str.extract(names_pattern, expand=True)
 
     nulls_found = mapping.isna()
 
     if nulls_found.all(axis=None):
         raise ValueError(
-            """
-            No labels in the columns
-            matched the regular expression
-            in names_pattern.
-            Kindly provide a regular expression
-            that matches all labels in the columns.
-            """
+            "No labels in the columns matched the regular expression "
+            "in names_pattern. Kindly provide a regular expression "
+            "that matches all labels in the columns."
         )
 
     if nulls_found.any(axis=None):
+        no_match = df.columns[nulls_found.any(axis="columns")]
         raise ValueError(
-            f"""
-            Not all labels in the columns
-            matched the regular expression
-            in names_pattern.Column Labels
-            {*df.columns[nulls_found.any(axis='columns')],}
-            could not be matched with the regex.
-            Kindly provide a regular expression
-            (with the correct groups) that matches all labels
-            in the columns.
-            """
+            f"Column labels {*no_match,} "
+            "could not be matched with any of the groups "
+            "in the provided regex. Kindly provide a regular expression "
+            "(with the correct groups) that matches all labels in the columns."
         )
 
     mapping.columns = names_to
-
-    if len(names_to) == 1:
-        mapping = mapping.squeeze()
-        df.columns = mapping
-        return _pivot_longer_frame_single_Index(
+    if ".value" not in names_to:
+        if len(names_to) == 1:
+            df.columns = mapping.iloc[:, 0]
+        else:
+            df.columns = pd.MultiIndex.from_frame(mapping)
+        return _pivot_longer_not_dot_value(
             df, index, sort_by_appearance, ignore_index, values_to
         )
 
-    if ".value" in names_to:
-        exclude = mapping[".value"].array
-        for word in names_to:
-            if (word != ".value") and (word in exclude):
-                raise ValueError(
-                    f"""
-                    `{word}` in names_to already exists
-                    in the new dataframe's columns.
-                    Kindly use a unique name.
-                    """
-                )
-
-    mapping_is_unique = not mapping.duplicated().any(axis=None).item()
-
-    if mapping_is_unique or (".value" not in names_to):
-        mapping = pd.MultiIndex.from_frame(mapping)
-    else:
-        cumcount = mapping.groupby(names_to).cumcount()
-        mapping = [series for _, series in mapping.items()]
-        mapping.append(cumcount)
-        mapping = pd.MultiIndex.from_arrays(mapping)
-    df.columns = mapping
+    # .value
 
-    return _pivot_longer_frame_MultiIndex(
-        df, index, sort_by_appearance, ignore_index, values_to
+    return _pivot_longer_dot_value(
+        df, index, sort_by_appearance, ignore_index, names_to, mapping
     )
 
 
 def _pivot_longer_names_sep(
     df: pd.DataFrame,
-    index,
+    index: Union[list, None],
     names_to: list,
     names_sep: Union[str, Pattern],
     values_to: str,
@@ -1248,47 +883,311 @@ def _pivot_longer_names_sep(
 
     if len_names_to != len_mapping_columns:
         raise ValueError(
-            f"""
-            The length of names_to does not match
-            the number of levels extracted.
-            The length of names_to is {len_names_to}
-            while the number of levels extracted is
-            {len_mapping_columns}.
-            """
+            f"The length of names_to does not match "
+            "the number of levels extracted. "
+            f"The length of names_to is {len_names_to} "
+            "while the number of levels extracted is "
+            f"{len_mapping_columns}."
         )
 
     mapping.columns = names_to
 
-    if ".value" in names_to:
-        exclude = mapping[".value"].array
-        for word in names_to:
-            if (word != ".value") and (word in exclude):
-                raise ValueError(
-                    f"""
-                    `{word}` in names_to already exists
-                    in the new dataframe's columns.
-                    Kindly use a unique name.
-                    """
-                )
+    if ".value" not in names_to:
+        df.columns = pd.MultiIndex.from_frame(mapping)
+        return _pivot_longer_not_dot_value(
+            df, index, sort_by_appearance, ignore_index, values_to
+        )
 
-    # having unique columns ensure the data can be recombined
-    # successfully via pd.concat; if the columns are not unique,
-    # a counter is created with cumcount to ensure uniqueness.
-    # This is dropped later on, and is not part of the final
-    # dataframe.
-    # This is relevant only for scenarios where `.value` is
-    # in names_to.
-    mapping_is_unique = not mapping.duplicated().any(axis=None).item()
+    # .value
+    return _pivot_longer_dot_value(
+        df, index, sort_by_appearance, ignore_index, names_to, mapping
+    )
 
-    if mapping_is_unique or (".value" not in names_to):
-        mapping = pd.MultiIndex.from_frame(mapping)
-    else:
-        cumcount = mapping.groupby(names_to).cumcount()
-        mapping = [series for _, series in mapping.items()]
-        mapping.append(cumcount)
-        mapping = pd.MultiIndex.from_arrays(mapping)
-    df.columns = mapping
 
-    return _pivot_longer_frame_MultiIndex(
-        df, index, sort_by_appearance, ignore_index, values_to
+@pf.register_dataframe_method
+def pivot_wider(
+    df: pd.DataFrame,
+    index: Optional[Union[List, str]] = None,
+    names_from: Optional[Union[List, str]] = None,
+    values_from: Optional[Union[List, str]] = None,
+    levels_order: Optional[list] = None,
+    flatten_levels: Optional[bool] = True,
+    names_sep="_",
+    names_glue: Callable = None,
+) -> pd.DataFrame:
+    """
+    Reshapes data from *long* to *wide* form.
+
+    The number of columns are increased, while decreasing
+    the number of rows. It is the inverse of the
+    [`pivot_longer`][janitor.functions.pivot.pivot_longer]
+    method, and is a wrapper around `pd.DataFrame.pivot` method.
+
+    This method does not mutate the original DataFrame.
+
+    Column selection in `index`, `names_from` and `values_from`
+    is possible using the
+    [`select_columns`][janitor.functions.select_columns.select_columns] syntax.
+
+    A ValueError is raised if the combination
+    of the `index` and `names_from` is not unique.
+
+    By default, values from `values_from` are always
+    at the top level if the columns are not flattened.
+    If flattened, the values from `values_from` are usually
+    at the start of each label in the columns.
+
+
+    Example:
+
+        >>> import pandas as pd
+        >>> import janitor
+        >>> df = [{'dep': 5.5, 'step': 1, 'a': 20, 'b': 30},
+        ...       {'dep': 5.5, 'step': 2, 'a': 25, 'b': 37},
+        ...       {'dep': 6.1, 'step': 1, 'a': 22, 'b': 19},
+        ...       {'dep': 6.1, 'step': 2, 'a': 18, 'b': 29}]
+        >>> df = pd.DataFrame(df)
+        >>> df
+           dep  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
+
+    Pivot and flatten columns:
+
+        >>> df.pivot_wider(
+        ...     index = "dep",
+        ...     names_from = "step",
+        ... )
+           dep  a_1  a_2  b_1  b_2
+        0  5.5   20   25   30   37
+        1  6.1   22   18   19   29
+
+    Change the order of the column labels:
+
+        >>> df.pivot_wider(
+        ...     index = "dep",
+        ...     names_from = "step",
+        ...     levels_order = ["step", None],
+        ... )
+           dep  1_a  2_a  1_b  2_b
+        0  5.5   20   25   30   37
+        1  6.1   22   18   19   29
+
+    Modify columns with `names_sep`:
+
+        >>> df.pivot_wider(
+        ...     index = "dep",
+        ...     names_from = "step",
+        ...     names_sep = "",
+        ... )
+           dep  a1  a2  b1  b2
+        0  5.5  20  25  30  37
+        1  6.1  22  18  19  29
+
+    Modify columns with `names_glue`:
+
+        >>> df.pivot_wider(
+        ...     index = "dep",
+        ...     names_from = "step",
+        ...     names_sep = None,
+        ...     names_glue = lambda col: f"{col[0]}_step{col[1]}",
+        ... )
+           dep  a_step1  a_step2  b_step1  b_step2
+        0  5.5       20       25       30       37
+        1  6.1       22       18       19       29
+
+
+    :param df: A pandas DataFrame.
+    :param index: Name(s) of columns to use as identifier variables.
+        It should be either a single column name, or a list of column names.
+        If `index` is not provided, the DataFrame's index is used.
+    :param names_from: Name(s) of column(s) to use to make the new
+        DataFrame's columns. Should be either a single column name,
+        or a list of column names.
+    :param values_from: Name(s) of column(s) that will be used for populating
+        the new DataFrame's values.
+        If `values_from` is not specified,  all remaining columns
+        will be used.
+    :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 `None` to represent that
+        level.
+    :param flatten_levels: Default is `True`. If `False`, the DataFrame stays
+        as a MultiIndex.
+    :param names_sep: If `names_from` or `values_from` contain multiple
+        variables, this will be used to join the values into a single string
+        to use as a column name. Default is `_`.
+        Applicable only if `flatten_levels` is `True`.
+    :param names_glue: A callable to control the output
+        of the flattened columns.
+        Applicable only if `flatten_levels` is `True`.
+        Function should be acceptable to pandas’ `map` function.
+    :returns: A pandas DataFrame that has been unpivoted from long to wide
+        form.
+    """
+
+    df = df.copy()
+
+    return _computations_pivot_wider(
+        df,
+        index,
+        names_from,
+        values_from,
+        levels_order,
+        flatten_levels,
+        names_sep,
+        names_glue,
+    )
+
+
+def _computations_pivot_wider(
+    df: pd.DataFrame,
+    index: Optional[Union[List, str]] = None,
+    names_from: Optional[Union[List, str]] = None,
+    values_from: Optional[Union[List, str]] = None,
+    levels_order: Optional[list] = None,
+    flatten_levels: Optional[bool] = True,
+    names_sep="_",
+    names_glue: Callable = None,
+) -> pd.DataFrame:
+    """
+    This is the main workhorse of the `pivot_wider` function.
+
+    It is a wrapper around `pd.pivot`. For a MultiIndex, the
+    order of the levels can be changed with `levels_order`.
+    The output for multiple `names_from` and/or `values_from`
+    can be controlled with `names_glue` and/or `names_sep`.
+
+    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().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
+    )
+
+    if levels_order and (isinstance(df.columns, pd.MultiIndex)):
+        df = df.reorder_levels(order=levels_order, axis="columns")
+
+    # an empty df is likely because
+    # there is no `values_from`
+    if any((df.empty, not flatten_levels)):
+        return df
+
+    # ensure all entries in names_from are strings
+    if (not names_from_all_strings) or (not column_dtype):
+        if isinstance(df.columns, pd.MultiIndex):
+            new_columns = [tuple(map(str, ent)) for ent in df]
+            df.columns = pd.MultiIndex.from_tuples(new_columns)
+        else:
+            df.columns = df.columns.astype(str)
+
+    if (names_sep is not None) and (isinstance(df.columns, pd.MultiIndex)):
+        df.columns = df.columns.map(names_sep.join)
+
+    if names_glue:
+        df.columns = df.columns.map(names_glue)
+
+    # if columns are of category type
+    # this returns columns to object dtype
+    # also, resetting index with category columns is not possible
+    df.columns = [*df.columns]
+
+    if index:
+        df = df.reset_index()
+
+    if df.columns.names:
+        df = df.rename_axis(columns=None)
+
+    return df
+
+
+def _data_checks_pivot_wider(
+    df,
+    index,
+    names_from,
+    values_from,
+    levels_order,
+    flatten_levels,
+    names_sep,
+    names_glue,
+):
+
+    """
+    This function raises errors if the arguments have the wrong
+    python type, or if the column does not exist in the dataframe.
+    This function is executed before proceeding to the computation phase.
+    Type annotations are not provided because this function is where type
+    checking happens.
+    """
+
+    if index is not None:
+        if is_list_like(index):
+            index = [*index]
+        index = _select_column_names(index, df)
+
+    if names_from is None:
+        raise ValueError(
+            "pivot_wider() is missing 1 required argument: 'names_from'"
+        )
+
+    if is_list_like(names_from):
+        names_from = [*names_from]
+    names_from = _select_column_names(names_from, df)
+
+    if values_from is not None:
+        if is_list_like(values_from):
+            values_from = [*values_from]
+        values_from = _select_column_names(values_from, df)
+        if len(values_from) == 1:
+            values_from = values_from[0]
+
+    if levels_order is not None:
+        check("levesl_order", levels_order, [list])
+
+    check("flatten_levels", flatten_levels, [bool])
+
+    if names_sep is not None:
+        check("names_sep", names_sep, [str])
+
+    if names_glue is not None:
+        check("names_glue", names_glue, [callable])
+
+    return (
+        df,
+        index,
+        names_from,
+        values_from,
+        levels_order,
+        flatten_levels,
+        names_sep,
+        names_glue,
     )


Test patch:
diff --git a/tests/functions/test_pivot_longer.py b/tests/functions/test_pivot_longer.py
--- a/tests/functions/test_pivot_longer.py
+++ b/tests/functions/test_pivot_longer.py
@@ -1,78 +1,19 @@
 import numpy as np
 import pandas as pd
 import pytest
-from pandas.testing import assert_frame_equal
 
-import re
+from pandas.testing import assert_frame_equal
 
 
 @pytest.fixture
 def df_checks():
-    return pd.DataFrame(
-        [
-            {"region": "Pacific", "2007": 1039, "2009": 2587},
-            {"region": "Southwest", "2007": 51, "2009": 176},
-            {"region": "Rocky Mountains and Plains", "2007": 200, "2009": 338},
-        ]
-    )
-
-
-@pytest.fixture
-def df_checks_output():
+    """fixture dataframe"""
     return pd.DataFrame(
         {
-            "region": [
-                "Pacific",
-                "Pacific",
-                "Southwest",
-                "Southwest",
-                "Rocky Mountains and Plains",
-                "Rocky Mountains and Plains",
-            ],
-            "year": ["2007", "2009", "2007", "2009", "2007", "2009"],
-            "num_nests": [1039, 2587, 51, 176, 200, 338],
-        }
-    )
-
-
-@pytest.fixture
-def test_df():
-    return pd.DataFrame(
-        {
-            "off_loc": ["A", "B", "C", "D", "E", "F"],
-            "pt_loc": ["G", "H", "I", "J", "K", "L"],
-            "pt_lat": [
-                100.07548220000001,
-                75.191326,
-                122.65134479999999,
-                124.13553329999999,
-                124.13553329999999,
-                124.01028909999998,
-            ],
-            "off_lat": [
-                121.271083,
-                75.93845266,
-                135.043791,
-                134.51128400000002,
-                134.484374,
-                137.962195,
-            ],
-            "pt_long": [
-                4.472089953,
-                -144.387785,
-                -40.45611048,
-                -46.07156181,
-                -46.07156181,
-                -46.01594293,
-            ],
-            "off_long": [
-                -7.188632000000001,
-                -143.2288569,
-                21.242563,
-                40.937416999999996,
-                40.78472,
-                22.905889000000002,
-            ],
+            "famid": [1, 1, 1, 2, 2, 2, 3, 3, 3],
+            "birth": [1, 2, 3, 1, 2, 3, 1, 2, 3],
+            "ht1": [2.8, 2.9, 2.2, 2, 1.8, 1.9, 2.2, 2.3, 2.1],
+            "ht2": [3.4, 3.8, 2.9, 3.2, 2.8, 2.4, 3.3, 3.4, 2.9],
         }
     )
 
@@ -89,165 +30,148 @@ def df_multi():
     )
 
 
-def test_names_to_in_index(df_checks):
-    """Raise TypeError if names_to intersects with index."""
-    with pytest.raises(ValueError):
-        df_checks.pivot_longer(index="region", names_to="region")
+def test_column_level_wrong_type(df_multi):
+    """Raise TypeError if wrong type is provided for column_level."""
+    with pytest.raises(TypeError):
+        df_multi.pivot_longer(index="name", column_level={0})
 
 
 def test_type_index(df_checks):
-    """Raise TypeError if wrong type is provided for the `index`."""
+    """Raise TypeError if wrong type is provided for the index."""
     with pytest.raises(TypeError):
         df_checks.pivot_longer(index=2007)
 
 
 def test_type_column_names(df_checks):
-    """Raise TypeError if wrong type is provided for `column_names`."""
+    """Raise TypeError if wrong type is provided for column_names."""
     with pytest.raises(TypeError):
         df_checks.pivot_longer(column_names=2007)
 
 
 def test_type_names_to(df_checks):
-    """Raise TypeError if wrong type is provided for `names_to`."""
+    """Raise TypeError if wrong type is provided for names_to."""
     with pytest.raises(TypeError):
         df_checks.pivot_longer(names_to={2007})
 
 
 def test_subtype_names_to(df_checks):
     """
-    Raise TypeError if `names_to` is a sequence
+    Raise TypeError if names_to is a sequence
     and the wrong type is provided for entries
-    in `names_to`.
+    in names_to.
     """
     with pytest.raises(TypeError):
-        df_checks.pivot_longer(names_to=[("year",)])
+        df_checks.pivot_longer(names_to=[("famid",)])
 
 
-def test_name_sep_wrong_type(test_df):
-    """Raise TypeError if the wrong type is provided for `names_sep`."""
-    with pytest.raises(TypeError):
-        test_df.pivot_longer(names_to=["set", ".value"], names_sep=["_"])
+def test_duplicate_names_to(df_checks):
+    """Raise error if names_to contains duplicates."""
+    with pytest.raises(ValueError, match="y already exists in names_to."):
+        df_checks.pivot_longer(names_to=["y", "y"], names_pattern="(.+)(.)")
 
 
-def test_name_sep_no_names_to(test_df):
-    """Raise ValuError if `names_sep` and names_to is None."""
+def test_both_names_sep_and_pattern(df_checks):
+    """
+    Raise ValueError if both names_sep
+    and names_pattern is provided.
+    """
     with pytest.raises(ValueError):
-        test_df.pivot_longer(names_to=None, names_sep="_")
+        df_checks.pivot_longer(
+            names_to=["rar", "bar"], names_sep="-", names_pattern="(.+)(.)"
+        )
 
 
-def test_name_pattern_wrong_type(test_df):
-    """Raise TypeError if the wrong type provided for `names_pattern`."""
+def test_name_pattern_wrong_type(df_checks):
+    """Raise TypeError if the wrong type provided for names_pattern."""
     with pytest.raises(TypeError):
-        test_df.pivot_longer(names_to=["set", ".value"], names_pattern=2007)
+        df_checks.pivot_longer(names_to=["rar", "bar"], names_pattern=2007)
 
 
-def test_name_pattern_no_names_to(test_df):
-    """Raise ValueError if `names_pattern` and names_to is None."""
+def test_name_pattern_no_names_to(df_checks):
+    """Raise ValueError if names_pattern and names_to is None."""
     with pytest.raises(ValueError):
-        test_df.pivot_longer(names_to=None, names_pattern="(.+)_(.+)")
+        df_checks.pivot_longer(names_to=None, names_pattern="(.+)(.)")
 
 
-def test_names_pattern_wrong_subtype(test_df):
+def test_name_pattern_groups_len(df_checks):
     """
-    Raise TypeError if `names_pattern` is a list/tuple
-    and wrong subtype is supplied.
+    Raise ValueError if names_pattern
+    and the number of groups
+    differs from the length of names_to.
     """
-    with pytest.raises(TypeError):
-        test_df.pivot_longer(names_to=["off", "pt"], names_pattern=[1, "pt"])
-
-
-def test_column_level_wrong_type(df_multi):
-    """Raise TypeError if wrong type is provided for `column_level`."""
-    with pytest.raises(TypeError):
-        df_multi.pivot_longer(index="name", column_level={0})
-
-
-def test_sort_by_appearance(test_df):
-    """Raise error if `sort_by_appearance` is not boolean."""
-    with pytest.raises(TypeError):
-        test_df.pivot_longer(
-            names_to=[".value", "value"],
-            names_sep="_",
-            sort_by_appearance="TRUE",
-        )
+    with pytest.raises(ValueError):
+        df_checks.pivot_longer(names_to=".value", names_pattern="(.+)(.)")
 
 
-def test_ignore_index(test_df):
-    """Raise error if `ignore_index` is not boolean."""
+def test_names_pattern_wrong_subtype(df_checks):
+    """
+    Raise TypeError if names_pattern is a list/tuple
+    and wrong subtype is supplied.
+    """
     with pytest.raises(TypeError):
-        test_df.pivot_longer(
-            names_to=[".value", "value"], names_sep="_", ignore_index="TRUE"
-        )
-
-
-def test_duplicate_names_to(test_df):
-    """Raise error if `names_to` contains duplicates."""
-    with pytest.raises(ValueError):
-        test_df.pivot_longer(
-            names_to=[".value", ".value"], names_pattern="(.+)_(.+)"
+        df_checks.pivot_longer(
+            names_to=["ht", "num"], names_pattern=[1, "\\d"]
         )
 
 
 def test_names_pattern_names_to_unequal_length(df_checks):
     """
-    Raise ValueError if `names_pattern` is a list/tuple
-    and wrong number of items in `names_to`.
+    Raise ValueError if names_pattern is a list/tuple
+    and wrong number of items in names_to.
     """
     with pytest.raises(ValueError):
         df_checks.pivot_longer(
-            names_to=["variable"], names_pattern=["1", "rar"]
+            names_to=["variable"], names_pattern=["^ht", ".+i.+"]
         )
 
 
 def test_names_pattern_names_to_dot_value(df_checks):
     """
-    Raise Error if `names_pattern` is a list/tuple and
-    `.value` in `names_to`.
+    Raise Error if names_pattern is a list/tuple and
+    .value in names_to.
     """
     with pytest.raises(ValueError):
         df_checks.pivot_longer(
-            names_to=["variable", ".value"], names_pattern=["1", "rar"]
+            names_to=["variable", ".value"], names_pattern=["^ht", ".+i.+"]
         )
 
 
-def test_both_names_sep_and_pattern(df_checks):
-    """
-    Raise ValueError if both `names_sep`
-    and `names_pattern` is provided.
-    """
-    with pytest.raises(ValueError):
-        df_checks.pivot_longer(
-            names_to=["rar", "bar"], names_sep="-", names_pattern=r"\\d+"
-        )
+def test_name_sep_wrong_type(df_checks):
+    """Raise TypeError if the wrong type is provided for names_sep."""
+    with pytest.raises(TypeError):
+        df_checks.pivot_longer(names_to=[".value", "num"], names_sep=["_"])
 
 
-def test_names_pattern_column_MultiIndex(df_multi):
-    """Raise ValueError if `names_pattern` and MultiIndex column"""
+def test_name_sep_no_names_to(df_checks):
+    """Raise ValueError if names_sep and names_to is None."""
     with pytest.raises(ValueError):
-        df_multi.pivot_longer(index="name", names_pattern=r"(.+)(.)")
+        df_checks.pivot_longer(names_to=None, names_sep="_")
+
+
+def test_values_to_wrong_type(df_checks):
+    """Raise TypeError if the wrong type is provided for `values_to`."""
+    with pytest.raises(TypeError):
+        df_checks.pivot_longer(values_to=["salvo"])
 
 
-def test_index_tuple_MultiIndex(df_multi):
+def test_values_to_exists_in_columns(df_checks):
     """
-    Raise ValueError if `index` is a tuple,
-    instead of a list of tuples,
-    and the dataframe's column is a MultiIndex.
+    Raise ValueError if values_to already
+    exists in the dataframe's columns.
     """
     with pytest.raises(ValueError):
-        df_multi.pivot_longer(index=("name", "a"))
+        df_checks.pivot_longer(values_to="birth")
 
 
-def test_column_names_tuple_MultiIndex(df_multi):
+def test_values_to_exists_in_names_to(df_checks):
     """
-    Raise ValueError if `column_names` is a tuple,
-    instead of a list of tuples,
-    and the dataframe's column is a MultiIndex.
+    Raise ValueError if values_to is in names_to.
     """
     with pytest.raises(ValueError):
-        df_multi.pivot_longer(column_names=("names", "aa"))
+        df_checks.pivot_longer(values_to="num", names_to="num")
 
 
-def test_column_MultiIndex_names_sep(df_multi):
+def test_column_multiindex_names_sep(df_multi):
     """
     Raise ValueError if the dataframe's column is a MultiIndex,
     and names_sep is present.
@@ -260,7 +184,7 @@ def test_column_MultiIndex_names_sep(df_multi):
         )
 
 
-def test_column_MultiIndex_names_pattern(df_multi):
+def test_column_multiindex_names_pattern(df_multi):
     """
     Raise ValueError if the dataframe's column is a MultiIndex,
     and names_pattern is present.
@@ -273,2069 +197,616 @@ def test_column_MultiIndex_names_pattern(df_multi):
         )
 
 
-def test_values_to_wrong_type(df_checks):
-    """Raise TypeError if the wrong type is provided for `values_to`."""
-    with pytest.raises(TypeError):
-        df_checks.pivot_longer(values_to=["salvo"])
-
-
-def test_values_to_exists_in_columns(df_checks):
+def test_index_tuple_multiindex(df_multi):
     """
-    Raise ValueError if `values_to` already
-    exists in the dataframe's columns.
+    Raise ValueError if index is a tuple,
+    instead of a list of tuples,
+    and the dataframe's column is a MultiIndex.
     """
     with pytest.raises(ValueError):
-        df_checks.pivot_longer(values_to="region")
+        df_multi.pivot_longer(index=("name", "a"))
 
 
-def test_values_to_exists_in_names_to(df_checks):
+def test_column_names_tuple_multiindex(df_multi):
     """
-    Raise ValueError if `values_to` is in names_to.
+    Raise ValueError if column_names is a tuple,
+    instead of a list of tuples,
+    and the dataframe's column is a MultiIndex.
     """
     with pytest.raises(ValueError):
-        df_checks.pivot_longer(values_to="year", names_to="year")
+        df_multi.pivot_longer(column_names=("names", "aa"))
 
 
-def test_MultiIndex_column_level(df_multi):
-    """Test output from MultiIndex column"""
-    result = df_multi.pivot_longer(
-        index="name", column_names="names", column_level=0
-    )
-    expected_output = df_multi.melt(
-        id_vars="name", value_vars="names", col_level=0
-    )
-    assert_frame_equal(result, expected_output)
+def test_sort_by_appearance(df_checks):
+    """Raise error if sort_by_appearance is not boolean."""
+    with pytest.raises(TypeError):
+        df_checks.pivot_longer(
+            names_to=[".value", "value"],
+            names_sep="_",
+            sort_by_appearance="TRUE",
+        )
 
 
-def test_pivot_no_args_passed():
-    """Test output if no arguments are passed."""
-    df_no_args = pd.DataFrame({"name": ["Wilbur", "Petunia", "Gregory"]})
+def test_ignore_index(df_checks):
+    """Raise error if ignore_index is not boolean."""
+    with pytest.raises(TypeError):
+        df_checks.pivot_longer(
+            names_to=[".value", "value"], names_sep="_", ignore_index="TRUE"
+        )
 
-    result = df_no_args.pivot_longer()
 
-    assert_frame_equal(result, df_no_args.melt())
+def test_names_to_index(df_checks):
+    """
+    Raise ValueError if there is no names_sep/names_pattern,
+    .value not in names_to and names_to intersects with index.
+    """
+    with pytest.raises(ValueError):
+        df_checks.pivot_longer(
+            names_to="famid",
+            index="famid",
+        )
 
 
-def test_pivot_index_only_and_sort_by_appearance(df_checks, df_checks_output):
+def test_names_sep_pattern_names_to_index(df_checks):
     """
-    Test output if only `index` is passed and
-    `sort_by_apperance is `True`.
+    Raise ValueError if names_sep/names_pattern,
+    .value not in names_to and names_to intersects with index.
     """
-    result = df_checks.pivot_longer(
-        index="region",
-        names_to="year",
-        values_to="num_nests",
-        sort_by_appearance=True,
-    )
-    assert_frame_equal(result, df_checks_output)
+    with pytest.raises(ValueError):
+        df_checks.pivot_longer(
+            names_to=["dim", "famid"],
+            names_sep="_",
+            index="famid",
+        )
 
 
-def test_pivot_index_only(df_checks):
-    """Test output if only `index` is passed."""
-    result = df_checks.pivot_longer(
-        index="region",
-        names_to="year",
-        values_to="num_nests",
-    )
+def test_dot_value_names_to_columns_intersect(df_checks):
+    """
+    Raise ValueError if names_sep/names_pattern,
+    .value in names_to,
+    and names_to intersects with the new columns
+    """
+    with pytest.raises(ValueError):
+        df_checks.pivot_longer(
+            index="famid", names_to=(".value", "ht"), names_pattern="(.+)(.)"
+        )
 
-    df_out = pd.DataFrame(
-        {
-            "region": [
-                "Pacific",
-                "Southwest",
-                "Rocky Mountains and Plains",
-                "Pacific",
-                "Southwest",
-                "Rocky Mountains and Plains",
-            ],
-            "year": ["2007", "2007", "2007", "2009", "2009", "2009"],
-            "num_nests": [1039, 51, 200, 2587, 176, 338],
-        }
-    )
 
-    assert_frame_equal(result, df_out)
+def test_dot_value_names_to_index_intersect(df_checks):
+    """
+    Raise ValueError if names_sep/names_pattern,
+    .value in names_to,
+    and names_to intersects with the index
+    """
+    with pytest.raises(ValueError):
+        df_checks.rename(columns={"famid": "ht"}).pivot_longer(
+            index="ht", names_to=(".value", "num"), names_pattern="(.+)(.)"
+        )
 
 
-def test_ignore_index_False():
+def test_names_pattern_list_empty_any(df_checks):
     """
-    Test dataframe output if `ignore_index` is False,
-    and `sort_by_appearance` is False.
+    Raise ValueError if names_pattern is a list,
+    and not all matches are returned.
     """
-    df_in = pd.DataFrame(
-        {
-            "A": {0: "a", 1: "b", 2: "c"},
-            "B": {0: 1, 1: 3, 2: 5},
-            "C": {0: 2, 1: 4, 2: 6},
-        }
-    )
+    with pytest.raises(ValueError):
+        df_checks.pivot_longer(
+            index=["famid", "birth"],
+            names_to=["ht"],
+            names_pattern=["rar"],
+        )
 
-    result = df_in.pivot_longer(
-        index="A",
-        column_names=["B", "C"],
-        ignore_index=False,
-        sort_by_appearance=False,
-    )
 
-    df_out = pd.DataFrame(
-        {
-            "A": ["a", "b", "c", "a", "b", "c"],
-            "variable": ["B", "B", "B", "C", "C", "C"],
-            "value": [1, 3, 5, 2, 4, 6],
-        },
-        index=pd.Int64Index([0, 1, 2, 0, 1, 2], dtype="int64"),
-    )
-
-    assert_frame_equal(result, df_out)
+def test_names_pattern_no_match(df_checks):
+    """Raise error if names_pattern is a regex and returns no matches."""
+    with pytest.raises(ValueError):
+        df_checks.pivot_longer(
+            index="famid",
+            names_to=[".value", "value"],
+            names_pattern=r"(rar)(.)",
+        )
 
 
-def test_ignore_index_false_same_length():
+def test_names_pattern_incomplete_match(df_checks):
     """
-    Test output when `ignore_index` is False
-    and the length of the new dataframe is the same
-    as the length of the original dataframe.
+    Raise error if names_pattern is a regex
+    and returns incomplete matches.
     """
-    df_in = pd.DataFrame(
-        {
-            "name": {
-                (67, 56): "Wilbur",
-                (80, 90): "Petunia",
-                (64, 50): "Gregory",
-            }
-        }
-    )
-    df_out = pd.DataFrame(
-        {
-            "variable": {(64, 50): "name", (67, 56): "name", (80, 90): "name"},
-            "value": {
-                (64, 50): "Gregory",
-                (67, 56): "Wilbur",
-                (80, 90): "Petunia",
-            },
-        }
-    )
+    with pytest.raises(ValueError):
+        df_checks.pivot_longer(
+            index="famid",
+            names_to=[".value", "value"],
+            names_pattern=r"(ht)(.)",
+        )
+
 
-    result = df_in.pivot_longer(ignore_index=False)
-    assert_frame_equal(result, df_out)
+def test_names_sep_len(df_checks):
+    """
+    Raise error if names_sep,
+    and the number of  matches returned
+    is not equal to the length of names_to.
+    """
+    with pytest.raises(ValueError):
+        df_checks.pivot_longer(names_to=".value", names_sep="(\\d)")
 
 
-def test_pivot_column_only(df_checks, df_checks_output):
-    """Test output if only `column_names` is passed."""
+def test_pivot_index_only(df_checks):
+    """Test output if only index is passed."""
     result = df_checks.pivot_longer(
-        column_names=["2007", "2009"],
-        names_to="year",
-        values_to="num_nests",
-        sort_by_appearance=True,
+        index=["famid", "birth"],
+        names_to="dim",
+        values_to="num",
     )
-    assert_frame_equal(result, df_checks_output)
-
 
-def test_pivot_index_patterns_only_sort_by_appearance(
-    df_checks, df_checks_output
-):
-    """
-    Test output if the `patterns` function is passed to `index`,
-    and `sort_by_appearance` is `True`.
-    """
-    result = df_checks.pivot_longer(
-        index=re.compile(r"[^\d+]"),
-        names_to="year",
-        values_to="num_nests",
-        sort_by_appearance=True,
+    actual = df_checks.melt(
+        ["famid", "birth"], var_name="dim", value_name="num"
     )
-    assert_frame_equal(result, df_checks_output)
 
+    assert_frame_equal(result, actual)
 
-def test_pivot_no_index_no_columns():
-    """Test output if neither `index`/`columns_names` is passed."""
-    test = pd.DataFrame(
-        {"1": ["fur", "lace"], "2": ["car", "plane"], "3": ["nsw", "vic"]}
+
+def test_pivot_column_only(df_checks):
+    """Test output if only column_names is passed."""
+    result = df_checks.pivot_longer(
+        column_names="ht*", names_to="dim", values_to="num", ignore_index=False
     )
-    result = test.pivot_longer()
-    expected_output = pd.DataFrame(
-        {
-            "variable": ["1", "1", "2", "2", "3", "3"],
-            "value": ["fur", "lace", "car", "plane", "nsw", "vic"],
-        }
+
+    actual = df_checks.melt(
+        ["famid", "birth"],
+        var_name="dim",
+        value_name="num",
+        ignore_index=False,
     )
-    assert_frame_equal(result, expected_output)
 
+    assert_frame_equal(result, actual)
 
-def test_pivot_columns_patterns_only(df_checks, df_checks_output):
-    """Test output if the `patterns` function is passed to `column_names`."""
+
+def test_pivot_sort_by_appearance(df_checks):
+    """Test output if only sort_by_appearance is True."""
     result = df_checks.pivot_longer(
-        column_names=re.compile(r"\d+"),
-        names_to="year",
-        values_to="num_nests",
+        column_names="ht*",
+        names_to="dim",
+        values_to="num",
         sort_by_appearance=True,
     )
-    assert_frame_equal(result, df_checks_output)
-
 
-@pytest.fixture
-def data():
-    return pd.DataFrame(
-        {
-            "country": ["United States", "Russia", "China"],
-            "vault_2012_f": [48.132, 46.366, 44.266],
-            "vault_2012_m": [46.632, 46.866, 48.316],
-            "vault_2016_f": [46.866, 45.733, 44.332],
-            "vault_2016_m": [45.865, 46.033, 45.0],
-            "floor_2012_f": [45.366, 41.599, 40.833],
-            "floor_2012_m": [45.266, 45.308, 45.133],
-            "floor_2016_f": [45.999, 42.032, 42.066],
-            "floor_2016_m": [43.757, 44.766, 43.799],
-        }
+    actual = (
+        df_checks.melt(
+            ["famid", "birth"],
+            var_name="dim",
+            value_name="num",
+            ignore_index=False,
+        )
+        .sort_index()
+        .reset_index(drop=True)
     )
 
+    assert_frame_equal(result, actual)
+
 
-def test_length_mismatch(data):
+def test_names_pat_str(df_checks):
     """
-    Raise error if `names_to` is a list/tuple,
-    and its length  does not match
-    the number of extracted columns.
+    Test output when names_pattern is a string,
+    and .value is present.
     """
+    result = (
+        df_checks.pivot_longer(
+            column_names="ht*",
+            names_to=(".value", "age"),
+            names_pattern="(.+)(.)",
+            sort_by_appearance=True,
+        )
+        .reindex(columns=["famid", "birth", "age", "ht"])
+        .astype({"age": int})
+    )
 
-    with pytest.raises(ValueError):
-        data.pivot_longer(names_to=["event", "year"], names_sep="_")
+    actual = pd.wide_to_long(
+        df_checks, stubnames="ht", i=["famid", "birth"], j="age"
+    ).reset_index()
 
+    assert_frame_equal(result, actual)
 
-def test_names_sep_pattern_names_to_index(data):
-    """
-    Raise ValueError if names_sep/names_pattern,
-    '.value' not in names_to
-    and names_to intersects with index
-    """
-    with pytest.raises(ValueError):
-        data.pivot_longer(
-            names_to=["event", "year", "country"],
-            names_sep="_",
-            index="country",
-        )
+
+def test_multiindex_column_level(df_multi):
+    """Test output from MultiIndex column"""
+    result = df_multi.pivot_longer(
+        index="name", column_names="names", column_level=0
+    )
+    expected_output = df_multi.melt(
+        id_vars="name", value_vars="names", col_level=0
+    )
+    assert_frame_equal(result, expected_output)
 
 
 @pytest.fixture
-def start():
+def test_df():
+    """Fixture DataFrame"""
     return pd.DataFrame(
         {
-            "id": [1, 2, 3],
-            "M_start_date_1": [201709, 201709, 201709],
-            "M_end_date_1": [201905, 201905, 201905],
-            "M_start_date_2": [202004, 202004, 202004],
-            "M_end_date_2": [202005, 202005, 202005],
-            "F_start_date_1": [201803, 201803, 201803],
-            "F_end_date_1": [201904, 201904, 201904],
-            "F_start_date_2": [201912, 201912, 201912],
-            "F_end_date_2": [202007, 202007, 202007],
+            "off_loc": ["A", "B", "C", "D", "E", "F"],
+            "pt_loc": ["G", "H", "I", "J", "K", "L"],
+            "pt_lat": [
+                100.07548220000001,
+                75.191326,
+                122.65134479999999,
+                124.13553329999999,
+                124.13553329999999,
+                124.01028909999998,
+            ],
+            "off_lat": [
+                121.271083,
+                75.93845266,
+                135.043791,
+                134.51128400000002,
+                134.484374,
+                137.962195,
+            ],
+            "pt_long": [
+                4.472089953,
+                -144.387785,
+                -40.45611048,
+                -46.07156181,
+                -46.07156181,
+                -46.01594293,
+            ],
+            "off_long": [
+                -7.188632000000001,
+                -143.2288569,
+                21.242563,
+                40.937416999999996,
+                40.78472,
+                22.905889000000002,
+            ],
         }
     )
 
 
-def test_dot_value_names_to_index(start):
-    """
-    Raise ValueError if names_sep/names_pattern,
-    '.value' in names_to,
-    and names_to intersects with index
-    """
-    with pytest.raises(ValueError):
-        start.pivot_longer(
-            "id",
-            names_to=("id", ".value"),
-            names_pattern="(M|F)_(start|end)_.+",
-        )
+def test_names_pattern_str(test_df):
+    """Test output for names_pattern and .value."""
 
+    result = test_df.pivot_longer(
+        column_names="*_*",
+        names_to=["set", ".value"],
+        names_pattern="(.+)_(.+)",
+        sort_by_appearance=True,
+    )
 
-def test_dot_value_names_to_columns_pattern(start):
-    """
-    Raise ValueError if names_pattern,
-    '.value' in names_to,
-    and names_to intersects with the new columns
-    """
-    with pytest.raises(ValueError):
-        start.pivot_longer(
-            "id",
-            names_to=("end", ".value"),
-            names_pattern="(M|F)_(start|end)_.+",
+    actual = test_df.copy()
+    actual.columns = actual.columns.str.split("_").str[::-1].str.join("_")
+    actual = (
+        pd.wide_to_long(
+            actual.reset_index(),
+            stubnames=["loc", "lat", "long"],
+            sep="_",
+            i="index",
+            j="set",
+            suffix=".+",
         )
+        .reset_index("set")
+        .reset_index(drop=True)
+    )
 
+    assert_frame_equal(result, actual)
 
-def test_dot_value_names_to_columns_sep(start):
-    """
-    Raise ValueError if names_sep,
-    '.value' in names_to,
-    and names_to intersects with the new columns
-    """
-    with pytest.raises(ValueError):
-        start.pivot_longer(
-            "id", names_to=("start", ".value", "num", "rar"), names_sep="_"
-        )
 
+def test_names_sep(test_df):
+    """Test output for names_sep and .value."""
 
-def test_empty_mapping_all(test_df):
-    """Raise error if `names_pattern` is a regex and returns no matches."""
-    with pytest.raises(ValueError):
-        test_df.pivot_longer(
-            names_to=[".value", "value"], names_pattern=r"(\d+)([A-Z])"
-        )
-
+    result = test_df.pivot_longer(
+        names_to=["set", ".value"], names_sep="_", sort_by_appearance=True
+    )
 
-def test_empty_mapping_any(test_df):
-    """
-    Raise error if `names_pattern` is a regex
-    and returns incomplete matches.
-    """
-    with pytest.raises(ValueError):
-        test_df.pivot_longer(
-            names_to=[".value", "value"], names_pattern=r"(.+)_(loc)"
+    actual = test_df.copy()
+    actual.columns = actual.columns.str.split("_").str[::-1].str.join("_")
+    actual = (
+        pd.wide_to_long(
+            actual.reset_index(),
+            stubnames=["loc", "lat", "long"],
+            sep="_",
+            i="index",
+            j="set",
+            suffix=".+",
         )
+        .reset_index("set")
+        .reset_index(drop=True)
+    )
 
+    assert_frame_equal(result, actual)
 
-def test_len_mapping_gt_len_names_to(test_df):
-    """
-    Raise error if `names_pattern` is a regex,
-    and the number of  matches returned
-    is more than length of `names_to`.
-    """
-    with pytest.raises(ValueError):
-        test_df.pivot_longer(
-            names_to=[".value", "value"], names_pattern="(.+)(_)(.+)"
-        )
 
+def test_names_pattern_list():
+    """Test output for names_pattern if list/tuple."""
 
-@pytest.fixture
-def names_pattern_list_df():
-    return pd.DataFrame(
-        [
-            {
-                "ID": 1,
-                "DateRange1Start": "1/1/90",
-                "DateRange1End": "3/1/90",
-                "Value1": 4.4,
-                "DateRange2Start": "4/5/91",
-                "DateRange2End": "6/7/91",
-                "Value2": 6.2,
-                "DateRange3Start": "5/5/95",
-                "DateRange3End": "6/6/96",
-                "Value3": 3.3,
-            }
-        ]
+    df = pd.DataFrame(
+        {
+            "Activity": ["P1", "P2"],
+            "General": ["AA", "BB"],
+            "m1": ["A1", "B1"],
+            "t1": ["TA1", "TB1"],
+            "m2": ["A2", "B2"],
+            "t2": ["TA2", "TB2"],
+            "m3": ["A3", "B3"],
+            "t3": ["TA3", "TB3"],
+        }
     )
 
+    result = df.pivot_longer(
+        index=["Activity", "General"],
+        names_pattern=["^m", "^t"],
+        names_to=["M", "Task"],
+        sort_by_appearance=True,
+    ).loc[:, ["Activity", "General", "Task", "M"]]
 
-def test_names_pattern_list_empty_all(names_pattern_list_df):
-    """
-    Raise ValueError if `names_pattern` is a list,
-    and nothing is returned.
-    """
-    with pytest.raises(ValueError):
-        names_pattern_list_df.pivot_longer(
-            index="ID",
-            names_to=("DateRangeStart", "DateRangeEnd", "Value"),
-            names_pattern=("^Start", "^End", "Value$"),
-        )
-
-
-def test_names_pattern_list_empty_any(names_pattern_list_df):
-    """
-    Raise ValueError if `names_pattern` is a list,
-    and not all matches are returned.
-    """
-    with pytest.raises(ValueError):
-        names_pattern_list_df.pivot_longer(
-            index="ID",
-            names_to=("DateRangeStart", "DateRangeEnd", "Value"),
-            names_pattern=("Start", "End", "Value$"),
+    actual = (
+        pd.wide_to_long(
+            df, i=["Activity", "General"], stubnames=["t", "m"], j="number"
         )
+        .set_axis(["Task", "M"], axis="columns")
+        .droplevel(-1)
+        .reset_index()
+    )
 
+    assert_frame_equal(result, actual)
 
-def test_names_pattern_list(names_pattern_list_df):
-    """Test output if `names_pattern` is a list."""
-    result = names_pattern_list_df.pivot_longer(
-        index="ID",
-        names_to=("DateRangeStart", "DateRangeEnd", "Value"),
-        names_pattern=("Start$", "End$", "^Value"),
-    )
 
-    expected_output = pd.DataFrame(
+@pytest.fixture
+def not_dot_value():
+    """Fixture DataFrame"""
+    return pd.DataFrame(
         {
-            "ID": [1, 1, 1],
-            "DateRangeStart": ["1/1/90", "4/5/91", "5/5/95"],
-            "DateRangeEnd": ["3/1/90", "6/7/91", "6/6/96"],
-            "Value": [4.4, 6.2, 3.3],
+            "country": ["United States", "Russia", "China"],
+            "vault_2012": [48.1, 46.4, 44.3],
+            "floor_2012": [45.4, 41.6, 40.8],
+            "vault_2016": [46.9, 45.7, 44.3],
+            "floor_2016": [46.0, 42.0, 42.1],
         }
     )
 
-    assert_frame_equal(result, expected_output)
 
+def test_not_dot_value_sep(not_dot_value):
+    """Test output when names_sep and no dot_value"""
 
-multiple_values_pattern = [
-    (
-        pd.DataFrame(
-            {
-                "country": ["United States", "Russia", "China"],
-                "vault2012": [48.1, 46.4, 44.3],
-                "floor2012": [45.4, 41.6, 40.8],
-                "vault2016": [46.9, 45.7, 44.3],
-                "floor2016": [46.0, 42.0, 42.1],
-            }
-        ),
-        pd.DataFrame(
-            [
-                {
-                    "country": "United States",
-                    "event": "vault",
-                    "year": "2012",
-                    "score": 48.1,
-                },
-                {
-                    "country": "United States",
-                    "event": "floor",
-                    "year": "2012",
-                    "score": 45.4,
-                },
-                {
-                    "country": "United States",
-                    "event": "vault",
-                    "year": "2016",
-                    "score": 46.9,
-                },
-                {
-                    "country": "United States",
-                    "event": "floor",
-                    "year": "2016",
-                    "score": 46.0,
-                },
-                {
-                    "country": "Russia",
-                    "event": "vault",
-                    "year": "2012",
-                    "score": 46.4,
-                },
-                {
-                    "country": "Russia",
-                    "event": "floor",
-                    "year": "2012",
-                    "score": 41.6,
-                },
-                {
-                    "country": "Russia",
-                    "event": "vault",
-                    "year": "2016",
-                    "score": 45.7,
-                },
-                {
-                    "country": "Russia",
-                    "event": "floor",
-                    "year": "2016",
-                    "score": 42.0,
-                },
-                {
-                    "country": "China",
-                    "event": "vault",
-                    "year": "2012",
-                    "score": 44.3,
-                },
-                {
-                    "country": "China",
-                    "event": "floor",
-                    "year": "2012",
-                    "score": 40.8,
-                },
-                {
-                    "country": "China",
-                    "event": "vault",
-                    "year": "2016",
-                    "score": 44.3,
-                },
-                {
-                    "country": "China",
-                    "event": "floor",
-                    "year": "2016",
-                    "score": 42.1,
-                },
-            ]
-        ),
+    result = not_dot_value.pivot_longer(
         "country",
-        ("event", "year"),
-        r"([A-Za-z]+)(\d+)",
-        True,
-    )
-]
-
-
-# probably unneccesary as it is just one item
-# the idea is that more tests will be added for this
-@pytest.mark.parametrize(
-    "df_in,df_out,index,names_to,names_pattern, sort_by_appearance",
-    multiple_values_pattern,
-)
-def test_multiple_values_pattern(
-    df_in, df_out, index, names_to, names_pattern, sort_by_appearance
-):
-    """
-    Test function to extract multiple columns,
-    using the `names_to` and `names_pattern` arguments.
-    """
-    result = df_in.pivot_longer(
-        index=index,
-        names_to=names_to,
-        names_pattern=names_pattern,
+        names_to=("event", "year"),
+        names_sep="_",
         values_to="score",
-        sort_by_appearance=sort_by_appearance,
+        sort_by_appearance=True,
     )
-    assert_frame_equal(result, df_out)
-
-
-multiple_values_sep = [
-    (
-        pd.DataFrame(
-            {
-                "country": ["United States", "Russia", "China"],
-                "vault_2012": [48.1, 46.4, 44.3],
-                "floor_2012": [45.4, 41.6, 40.8],
-                "vault_2016": [46.9, 45.7, 44.3],
-                "floor_2016": [46.0, 42.0, 42.1],
-            }
-        ),
-        pd.DataFrame(
-            [
-                {
-                    "country": "United States",
-                    "event": "vault",
-                    "year": "2012",
-                    "score": 48.1,
-                },
-                {
-                    "country": "United States",
-                    "event": "floor",
-                    "year": "2012",
-                    "score": 45.4,
-                },
-                {
-                    "country": "United States",
-                    "event": "vault",
-                    "year": "2016",
-                    "score": 46.9,
-                },
-                {
-                    "country": "United States",
-                    "event": "floor",
-                    "year": "2016",
-                    "score": 46.0,
-                },
-                {
-                    "country": "Russia",
-                    "event": "vault",
-                    "year": "2012",
-                    "score": 46.4,
-                },
-                {
-                    "country": "Russia",
-                    "event": "floor",
-                    "year": "2012",
-                    "score": 41.6,
-                },
-                {
-                    "country": "Russia",
-                    "event": "vault",
-                    "year": "2016",
-                    "score": 45.7,
-                },
-                {
-                    "country": "Russia",
-                    "event": "floor",
-                    "year": "2016",
-                    "score": 42.0,
-                },
-                {
-                    "country": "China",
-                    "event": "vault",
-                    "year": "2012",
-                    "score": 44.3,
-                },
-                {
-                    "country": "China",
-                    "event": "floor",
-                    "year": "2012",
-                    "score": 40.8,
-                },
-                {
-                    "country": "China",
-                    "event": "vault",
-                    "year": "2016",
-                    "score": 44.3,
-                },
-                {
-                    "country": "China",
-                    "event": "floor",
-                    "year": "2016",
-                    "score": 42.1,
-                },
-            ]
-        ),
-        "country",
-        ("event", "year"),
-        "_",
-        True,
-    ),
-    (
-        pd.DataFrame(
-            {
-                "country": ["United States", "Russia", "China"],
-                "vault_2012_f": [
-                    48.132,
-                    46.36600000000001,
-                    44.266000000000005,
-                ],
-                "vault_2012_m": [46.632, 46.86600000000001, 48.316],
-                "vault_2016_f": [
-                    46.86600000000001,
-                    45.733000000000004,
-                    44.332,
-                ],
-                "vault_2016_m": [45.865, 46.033, 45.0],
-                "floor_2012_f": [45.36600000000001, 41.599, 40.833],
-                "floor_2012_m": [45.266000000000005, 45.308, 45.133],
-                "floor_2016_f": [45.998999999999995, 42.032, 42.066],
-                "floor_2016_m": [43.757, 44.766000000000005, 43.799],
-            }
-        ),
-        pd.DataFrame(
-            [
-                {
-                    "country": "United States",
-                    "event": "vault",
-                    "year": "2012",
-                    "gender": "f",
-                    "score": 48.132,
-                },
-                {
-                    "country": "United States",
-                    "event": "vault",
-                    "year": "2012",
-                    "gender": "m",
-                    "score": 46.632,
-                },
-                {
-                    "country": "United States",
-                    "event": "vault",
-                    "year": "2016",
-                    "gender": "f",
-                    "score": 46.86600000000001,
-                },
-                {
-                    "country": "United States",
-                    "event": "vault",
-                    "year": "2016",
-                    "gender": "m",
-                    "score": 45.865,
-                },
-                {
-                    "country": "United States",
-                    "event": "floor",
-                    "year": "2012",
-                    "gender": "f",
-                    "score": 45.36600000000001,
-                },
-                {
-                    "country": "United States",
-                    "event": "floor",
-                    "year": "2012",
-                    "gender": "m",
-                    "score": 45.266000000000005,
-                },
-                {
-                    "country": "United States",
-                    "event": "floor",
-                    "year": "2016",
-                    "gender": "f",
-                    "score": 45.998999999999995,
-                },
-                {
-                    "country": "United States",
-                    "event": "floor",
-                    "year": "2016",
-                    "gender": "m",
-                    "score": 43.757,
-                },
-                {
-                    "country": "Russia",
-                    "event": "vault",
-                    "year": "2012",
-                    "gender": "f",
-                    "score": 46.36600000000001,
-                },
-                {
-                    "country": "Russia",
-                    "event": "vault",
-                    "year": "2012",
-                    "gender": "m",
-                    "score": 46.86600000000001,
-                },
-                {
-                    "country": "Russia",
-                    "event": "vault",
-                    "year": "2016",
-                    "gender": "f",
-                    "score": 45.733000000000004,
-                },
-                {
-                    "country": "Russia",
-                    "event": "vault",
-                    "year": "2016",
-                    "gender": "m",
-                    "score": 46.033,
-                },
-                {
-                    "country": "Russia",
-                    "event": "floor",
-                    "year": "2012",
-                    "gender": "f",
-                    "score": 41.599,
-                },
-                {
-                    "country": "Russia",
-                    "event": "floor",
-                    "year": "2012",
-                    "gender": "m",
-                    "score": 45.308,
-                },
-                {
-                    "country": "Russia",
-                    "event": "floor",
-                    "year": "2016",
-                    "gender": "f",
-                    "score": 42.032,
-                },
-                {
-                    "country": "Russia",
-                    "event": "floor",
-                    "year": "2016",
-                    "gender": "m",
-                    "score": 44.766000000000005,
-                },
-                {
-                    "country": "China",
-                    "event": "vault",
-                    "year": "2012",
-                    "gender": "f",
-                    "score": 44.266000000000005,
-                },
-                {
-                    "country": "China",
-                    "event": "vault",
-                    "year": "2012",
-                    "gender": "m",
-                    "score": 48.316,
-                },
-                {
-                    "country": "China",
-                    "event": "vault",
-                    "year": "2016",
-                    "gender": "f",
-                    "score": 44.332,
-                },
-                {
-                    "country": "China",
-                    "event": "vault",
-                    "year": "2016",
-                    "gender": "m",
-                    "score": 45.0,
-                },
-                {
-                    "country": "China",
-                    "event": "floor",
-                    "year": "2012",
-                    "gender": "f",
-                    "score": 40.833,
-                },
-                {
-                    "country": "China",
-                    "event": "floor",
-                    "year": "2012",
-                    "gender": "m",
-                    "score": 45.133,
-                },
-                {
-                    "country": "China",
-                    "event": "floor",
-                    "year": "2016",
-                    "gender": "f",
-                    "score": 42.066,
-                },
-                {
-                    "country": "China",
-                    "event": "floor",
-                    "year": "2016",
-                    "gender": "m",
-                    "score": 43.799,
-                },
-            ]
-        ),
+    result = result.sort_values(
+        ["country", "event", "year"], ignore_index=True
+    )
+    actual = not_dot_value.set_index("country")
+    actual.columns = actual.columns.str.split("_", expand=True)
+    actual.columns.names = ["event", "year"]
+    actual = (
+        actual.stack(["event", "year"])
+        .rename("score")
+        .sort_index()
+        .reset_index()
+    )
+
+    assert_frame_equal(result, actual)
+
+
+def test_not_dot_value_pattern(not_dot_value):
+    """Test output when names_pattern is a string and no dot_value"""
+
+    result = not_dot_value.pivot_longer(
         "country",
-        ("event", "year", "gender"),
-        "_",
-        True,
-    ),
-]
-
-
-@pytest.mark.parametrize(
-    "df_in,df_out,index,names_to,names_sep,sort_by_appearance",
-    multiple_values_sep,
-)
-def test_multiple_values_sep(
-    df_in, df_out, index, names_to, names_sep, sort_by_appearance
-):
-    """
-    Test function to extract multiple columns,
-    using the `names_to` and `names_sep` arguments.
-    """
-    result = df_in.pivot_longer(
-        index=index,
-        names_to=names_to,
-        names_sep=names_sep,
+        names_to=("event", "year"),
+        names_pattern=r"(.+)_(.+)",
         values_to="score",
-        sort_by_appearance=sort_by_appearance,
+        sort_by_appearance=True,
     )
-    assert_frame_equal(result, df_out)
-
-
-paired_columns_pattern = [
-    (
-        pd.DataFrame(
-            {
-                "id": [1, 2, 3],
-                "M_start_date_1": [201709, 201709, 201709],
-                "M_end_date_1": [201905, 201905, 201905],
-                "M_start_date_2": [202004, 202004, 202004],
-                "M_end_date_2": [202005, 202005, 202005],
-                "F_start_date_1": [201803, 201803, 201803],
-                "F_end_date_1": [201904, 201904, 201904],
-                "F_start_date_2": [201912, 201912, 201912],
-                "F_end_date_2": [202007, 202007, 202007],
-            }
-        ),
-        pd.DataFrame(
-            [
-                {"id": 1, "cod": "M", "start": 201709, "end": 201905},
-                {"id": 1, "cod": "M", "start": 202004, "end": 202005},
-                {"id": 1, "cod": "F", "start": 201803, "end": 201904},
-                {"id": 1, "cod": "F", "start": 201912, "end": 202007},
-                {"id": 2, "cod": "M", "start": 201709, "end": 201905},
-                {"id": 2, "cod": "M", "start": 202004, "end": 202005},
-                {"id": 2, "cod": "F", "start": 201803, "end": 201904},
-                {"id": 2, "cod": "F", "start": 201912, "end": 202007},
-                {"id": 3, "cod": "M", "start": 201709, "end": 201905},
-                {"id": 3, "cod": "M", "start": 202004, "end": 202005},
-                {"id": 3, "cod": "F", "start": 201803, "end": 201904},
-                {"id": 3, "cod": "F", "start": 201912, "end": 202007},
-            ]
-        ),
-        "id",
-        ("cod", ".value"),
-        "(M|F)_(start|end)_.+",
-        True,
-    ),
-    (
-        pd.DataFrame(
-            {
-                "person_id": [1, 2, 3],
-                "date1": ["12/31/2007", "11/25/2009", "10/06/2005"],
-                "val1": [2, 4, 6],
-                "date2": ["12/31/2017", "11/25/2019", "10/06/2015"],
-                "val2": [1, 3, 5],
-                "date3": ["12/31/2027", "11/25/2029", "10/06/2025"],
-                "val3": [7, 9, 11],
-            }
-        ),
-        pd.DataFrame(
-            [
-                {"person_id": 1, "value": "1", "date": "12/31/2007", "val": 2},
-                {"person_id": 1, "value": "2", "date": "12/31/2017", "val": 1},
-                {"person_id": 1, "value": "3", "date": "12/31/2027", "val": 7},
-                {"person_id": 2, "value": "1", "date": "11/25/2009", "val": 4},
-                {"person_id": 2, "value": "2", "date": "11/25/2019", "val": 3},
-                {"person_id": 2, "value": "3", "date": "11/25/2029", "val": 9},
-                {"person_id": 3, "value": "1", "date": "10/06/2005", "val": 6},
-                {"person_id": 3, "value": "2", "date": "10/06/2015", "val": 5},
-                {
-                    "person_id": 3,
-                    "value": "3",
-                    "date": "10/06/2025",
-                    "val": 11,
-                },
-            ]
-        ),
-        re.compile("^(?!(date|val))"),
-        (".value", "value"),
-        r"([a-z]+)(\d)",
-        True,
-    ),
-    (
-        pd.DataFrame(
-            [
-                {
-                    "id": 1,
-                    "a1": "a",
-                    "a2": "b",
-                    "a3": "c",
-                    "A1": "A",
-                    "A2": "B",
-                    "A3": "C",
-                }
-            ]
-        ),
-        pd.DataFrame(
-            {
-                "id": [1, 1, 1],
-                "instance": ["1", "2", "3"],
-                "a": ["a", "b", "c"],
-                "A": ["A", "B", "C"],
-            },
-        ),
-        "id",
-        (".value", "instance"),
-        r"(\w)(\d)",
-        True,
-    ),
-    (
-        pd.DataFrame(
-            {
-                "A1970": ["a", "b", "c"],
-                "A1980": ["d", "e", "f"],
-                "B1970": [2.5, 1.2, 0.7],
-                "B1980": [3.2, 1.3, 0.1],
-                "X": [-1.085631, 0.997345, 0.282978],
-            }
-        ),
-        pd.DataFrame(
-            {
-                "X": [
-                    -1.085631,
-                    -1.085631,
-                    0.997345,
-                    0.997345,
-                    0.282978,
-                    0.282978,
-                ],
-                "year": ["1970", "1980", "1970", "1980", "1970", "1980"],
-                "A": ["a", "d", "b", "e", "c", "f"],
-                "B": [2.5, 3.2, 1.2, 1.3, 0.7, 0.1],
-            }
-        ),
-        "X",
-        (".value", "year"),
-        "([A-Z])(.+)",
-        True,
-    ),
-    (
-        pd.DataFrame(
-            {
-                "id": ["A", "B", "C", "D", "E", "F"],
-                "f_start": ["p", "i", "i", "p", "p", "i"],
-                "d_start": [
-                    "2018-01-01",
-                    "2019-04-01",
-                    "2018-06-01",
-                    "2019-12-01",
-                    "2019-02-01",
-                    "2018-04-01",
-                ],
-                "f_end": ["p", "p", "i", "p", "p", "i"],
-                "d_end": [
-                    "2018-02-01",
-                    "2020-01-01",
-                    "2019-03-01",
-                    "2020-05-01",
-                    "2019-05-01",
-                    "2018-07-01",
-                ],
-            }
-        ),
-        pd.DataFrame(
-            [
-                {"id": "A", "status": "start", "f": "p", "d": "2018-01-01"},
-                {"id": "A", "status": "end", "f": "p", "d": "2018-02-01"},
-                {"id": "B", "status": "start", "f": "i", "d": "2019-04-01"},
-                {"id": "B", "status": "end", "f": "p", "d": "2020-01-01"},
-                {"id": "C", "status": "start", "f": "i", "d": "2018-06-01"},
-                {"id": "C", "status": "end", "f": "i", "d": "2019-03-01"},
-                {"id": "D", "status": "start", "f": "p", "d": "2019-12-01"},
-                {"id": "D", "status": "end", "f": "p", "d": "2020-05-01"},
-                {"id": "E", "status": "start", "f": "p", "d": "2019-02-01"},
-                {"id": "E", "status": "end", "f": "p", "d": "2019-05-01"},
-                {"id": "F", "status": "start", "f": "i", "d": "2018-04-01"},
-                {"id": "F", "status": "end", "f": "i", "d": "2018-07-01"},
-            ]
-        ),
-        "id",
-        (".value", "status"),
-        "(.*)_(.*)",
-        True,
-    ),
-]
-
-
-@pytest.mark.parametrize(
-    """
-    df_in,df_out,index,names_to,names_pattern,
-    sort_by_appearance
-    """,
-    paired_columns_pattern,
-)
-def test_extract_column_names_pattern(
-    df_in,
-    df_out,
-    index,
-    names_to,
-    names_pattern,
-    sort_by_appearance,
-):
-    """
-    Test output if `.value` is in the `names_to`
-    argument and `names_pattern` is used.
-    """
-    result = df_in.pivot_longer(
-        index=index,
-        names_to=names_to,
-        names_pattern=names_pattern,
-        sort_by_appearance=sort_by_appearance,
+    result = result.sort_values(
+        ["country", "event", "year"], ignore_index=True
     )
-    assert_frame_equal(result, df_out)
-
-
-paired_columns_sep = [
-    # https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.wide_to_long.html
-    (
-        pd.DataFrame(
-            {
-                "A(weekly)-2010": [0.548814, 0.7151890000000001, 0.602763],
-                "A(weekly)-2011": [0.544883, 0.423655, 0.645894],
-                "B(weekly)-2010": [
-                    0.437587,
-                    0.8917729999999999,
-                    0.9636629999999999,
-                ],
-                "B(weekly)-2011": [0.383442, 0.791725, 0.528895],
-                "X": [0, 1, 1],
-            }
-        ),
-        pd.DataFrame(
-            {
-                "X": [0, 0, 1, 1, 1, 1],
-                "year": ["2010", "2011", "2010", "2011", "2010", "2011"],
-                "A(weekly)": [
-                    0.548814,
-                    0.544883,
-                    0.7151890000000001,
-                    0.423655,
-                    0.602763,
-                    0.645894,
-                ],
-                "B(weekly)": [
-                    0.437587,
-                    0.383442,
-                    0.8917729999999999,
-                    0.791725,
-                    0.9636629999999999,
-                    0.528895,
-                ],
-            }
-        ),
-        "X",
-        (".value", "year"),
-        "-",
-        True,
-    ),
-    (
-        pd.DataFrame(
-            {
-                "indexer": [0, 1],
-                "S_1": [1, 1],
-                "S_2": [0, 1],
-                "S_3": ["0", np.nan],
-                "S_4": ["1", np.nan],
-            }
-        ),
-        pd.DataFrame(
-            {
-                "indexer": [0, 0, 0, 0, 1, 1, 1, 1],
-                "num": ["1", "2", "3", "4", "1", "2", "3", "4"],
-                "S": [1, 0, "0", "1", 1, 1, np.nan, np.nan],
-            }
-        ),
-        "indexer",
-        (".value", "num"),
-        "_",
-        True,
-    ),
-    (
-        pd.DataFrame(
-            {
-                "county": [1001, 1003, 1005],
-                "area": [275, 394, 312],
-                "pop_2006": [1037, 2399, 1638],
-                "pop_2007": [1052, 2424, 1647],
-                "pop_2008": [1102, 2438, 1660],
-            }
-        ),
-        pd.DataFrame(
-            {
-                "county": [
-                    1001,
-                    1001,
-                    1001,
-                    1003,
-                    1003,
-                    1003,
-                    1005,
-                    1005,
-                    1005,
-                ],
-                "area": [275, 275, 275, 394, 394, 394, 312, 312, 312],
-                "year": [
-                    "2006",
-                    "2007",
-                    "2008",
-                    "2006",
-                    "2007",
-                    "2008",
-                    "2006",
-                    "2007",
-                    "2008",
-                ],
-                "pop": [1037, 1052, 1102, 2399, 2424, 2438, 1638, 1647, 1660],
-            }
-        ),
-        ["county", "area"],
-        (".value", "year"),
-        "_",
-        True,
-    ),
-    (
-        pd.DataFrame(
-            {
-                "family": [1, 2, 3, 4, 5],
-                "dob_child1": [
-                    "1998-11-26",
-                    "1996-06-22",
-                    "2002-07-11",
-                    "2004-10-10",
-                    "2000-12-05",
-                ],
-                "dob_child2": [
-                    "2000-01-29",
-                    np.nan,
-                    "2004-04-05",
-                    "2009-08-27",
-                    "2005-02-28",
-                ],
-                "gender_child1": [1, 2, 2, 1, 2],
-                "gender_child2": [2.0, np.nan, 2.0, 1.0, 1.0],
-            }
-        ),
-        pd.DataFrame(
-            [
-                {
-                    "family": 1,
-                    "child": "child1",
-                    "dob": "1998-11-26",
-                    "gender": 1.0,
-                },
-                {
-                    "family": 1,
-                    "child": "child2",
-                    "dob": "2000-01-29",
-                    "gender": 2.0,
-                },
-                {
-                    "family": 2,
-                    "child": "child1",
-                    "dob": "1996-06-22",
-                    "gender": 2.0,
-                },
-                {
-                    "family": 2,
-                    "child": "child2",
-                    "dob": np.nan,
-                    "gender": np.nan,
-                },
-                {
-                    "family": 3,
-                    "child": "child1",
-                    "dob": "2002-07-11",
-                    "gender": 2.0,
-                },
-                {
-                    "family": 3,
-                    "child": "child2",
-                    "dob": "2004-04-05",
-                    "gender": 2.0,
-                },
-                {
-                    "family": 4,
-                    "child": "child1",
-                    "dob": "2004-10-10",
-                    "gender": 1.0,
-                },
-                {
-                    "family": 4,
-                    "child": "child2",
-                    "dob": "2009-08-27",
-                    "gender": 1.0,
-                },
-                {
-                    "family": 5,
-                    "child": "child1",
-                    "dob": "2000-12-05",
-                    "gender": 2.0,
-                },
-                {
-                    "family": 5,
-                    "child": "child2",
-                    "dob": "2005-02-28",
-                    "gender": 1.0,
-                },
-            ]
-        ),
-        "family",
-        (".value", "child"),
-        "_",
-        True,
-    ),
-    (
-        pd.DataFrame(
-            {
-                "dob_child2": [
-                    "2000-01-29",
-                    np.nan,
-                    "2004-04-05",
-                    "2009-08-27",
-                    "2005-02-28",
-                ],
-                "gender_child1": [1, 2, 2, 1, 2],
-                "gender_child2": [2.0, np.nan, 2.0, 1.0, 1.0],
-                "dob_child1": [
-                    "1998-11-26",
-                    "1996-06-22",
-                    "2002-07-11",
-                    "2004-10-10",
-                    "2000-12-05",
-                ],
-            }
-        ),
-        pd.DataFrame(
-            {
-                "child": [
-                    "child2",
-                    "child1",
-                    "child2",
-                    "child1",
-                    "child2",
-                    "child1",
-                    "child2",
-                    "child1",
-                    "child2",
-                    "child1",
-                ],
-                "dob": [
-                    "2000-01-29",
-                    "1998-11-26",
-                    np.nan,
-                    "1996-06-22",
-                    "2004-04-05",
-                    "2002-07-11",
-                    "2009-08-27",
-                    "2004-10-10",
-                    "2005-02-28",
-                    "2000-12-05",
-                ],
-                "gender": [2.0, 1, np.nan, 2, 2.0, 2, 1.0, 1, 1.0, 2],
-            }
-        ),
-        None,
-        (".value", "child"),
-        "_",
-        True,
-    ),
-    (
-        pd.DataFrame(
-            [
-                {
-                    "id": "A",
-                    "Q1r1_pepsi": 1,
-                    "Q1r1_cola": 0,
-                    "Q1r2_pepsi": 1,
-                    "Q1r2_cola": 0,
-                },
-                {
-                    "id": "B",
-                    "Q1r1_pepsi": 0,
-                    "Q1r1_cola": 0,
-                    "Q1r2_pepsi": 1,
-                    "Q1r2_cola": 1,
-                },
-                {
-                    "id": "C",
-                    "Q1r1_pepsi": 1,
-                    "Q1r1_cola": 1,
-                    "Q1r2_pepsi": 1,
-                    "Q1r2_cola": 1,
-                },
-            ]
-        ),
-        pd.DataFrame(
-            [
-                {"id": "A", "brand": "pepsi", "Q1r1": 1, "Q1r2": 1},
-                {"id": "A", "brand": "cola", "Q1r1": 0, "Q1r2": 0},
-                {"id": "B", "brand": "pepsi", "Q1r1": 0, "Q1r2": 1},
-                {"id": "B", "brand": "cola", "Q1r1": 0, "Q1r2": 1},
-                {"id": "C", "brand": "pepsi", "Q1r1": 1, "Q1r2": 1},
-                {"id": "C", "brand": "cola", "Q1r1": 1, "Q1r2": 1},
-            ]
-        ),
-        "id",
-        (".value", "brand"),
-        "_",
-        True,
-    ),
-    (
-        pd.DataFrame(
-            {
-                "event": [1, 2],
-                "url_1": ["g1", "g3"],
-                "name_1": ["dc", "nyc"],
-                "url_2": ["g2", "g4"],
-                "name_2": ["sf", "la"],
-            }
-        ),
-        pd.DataFrame(
-            {
-                "event": [1, 1, 2, 2],
-                "item": ["1", "2", "1", "2"],
-                "url": ["g1", "g2", "g3", "g4"],
-                "name": ["dc", "sf", "nyc", "la"],
-            }
-        ),
-        "event",
-        (".value", "item"),
-        "_",
-        True,
-    ),
-]
-
-
-@pytest.mark.parametrize(
-    """
-    df_in,df_out,index,names_to,names_sep,
-    sort_by_appearance
-    """,
-    paired_columns_sep,
-)
-def test_extract_column_names_sep(
-    df_in,
-    df_out,
-    index,
-    names_to,
-    names_sep,
-    sort_by_appearance,
-):
-    """
-    Test output if `.value` is in the `names_to` argument
-    and `names_sep` is used.
-    """
-    result = df_in.pivot_longer(
-        index=index,
-        names_to=names_to,
-        names_sep=names_sep,
-        sort_by_appearance=sort_by_appearance,
+    actual = not_dot_value.set_index("country")
+    actual.columns = actual.columns.str.split("_", expand=True)
+    actual.columns.names = ["event", "year"]
+    actual = (
+        actual.stack(["event", "year"])
+        .rename("score")
+        .sort_index()
+        .reset_index()
     )
-    assert_frame_equal(result, df_out)
-
-
-# https://community.rstudio.com/t/pivot-longer-on-multiple-column-sets-pairs/43958/10
-paired_columns_no_index_pattern = [
-    (
-        pd.DataFrame(
-            {
-                "off_loc": ["A", "B", "C", "D", "E", "F"],
-                "pt_loc": ["G", "H", "I", "J", "K", "L"],
-                "pt_lat": [
-                    100.07548220000001,
-                    75.191326,
-                    122.65134479999999,
-                    124.13553329999999,
-                    124.13553329999999,
-                    124.01028909999998,
-                ],
-                "off_lat": [
-                    121.271083,
-                    75.93845266,
-                    135.043791,
-                    134.51128400000002,
-                    134.484374,
-                    137.962195,
-                ],
-                "pt_long": [
-                    4.472089953,
-                    -144.387785,
-                    -40.45611048,
-                    -46.07156181,
-                    -46.07156181,
-                    -46.01594293,
-                ],
-                "off_long": [
-                    -7.188632000000001,
-                    -143.2288569,
-                    21.242563,
-                    40.937416999999996,
-                    40.78472,
-                    22.905889000000002,
-                ],
-            }
-        ),
-        pd.DataFrame(
-            {
-                "set": [
-                    "off",
-                    "pt",
-                    "off",
-                    "pt",
-                    "off",
-                    "pt",
-                    "off",
-                    "pt",
-                    "off",
-                    "pt",
-                    "off",
-                    "pt",
-                ],
-                "loc": [
-                    "A",
-                    "G",
-                    "B",
-                    "H",
-                    "C",
-                    "I",
-                    "D",
-                    "J",
-                    "E",
-                    "K",
-                    "F",
-                    "L",
-                ],
-                "lat": [
-                    121.271083,
-                    100.07548220000001,
-                    75.93845266,
-                    75.191326,
-                    135.043791,
-                    122.65134479999999,
-                    134.51128400000002,
-                    124.13553329999999,
-                    134.484374,
-                    124.13553329999999,
-                    137.962195,
-                    124.01028909999998,
-                ],
-                "long": [
-                    -7.188632000000001,
-                    4.472089953,
-                    -143.2288569,
-                    -144.387785,
-                    21.242563,
-                    -40.45611048,
-                    40.937416999999996,
-                    -46.07156181,
-                    40.78472,
-                    -46.07156181,
-                    22.905889000000002,
-                    -46.01594293,
-                ],
-            }
-        ),
-        ("set", ".value"),
-        "(.+)_(.+)",
-        True,
-    )
-]
 
+    assert_frame_equal(result, actual)
 
-@pytest.mark.parametrize(
-    """
-    df_in,df_out,names_to,names_pattern,
-    sort_by_appearance
-    """,
-    paired_columns_no_index_pattern,
-)
-def test_paired_columns_no_index_pattern(
-    df_in, df_out, names_to, names_pattern, sort_by_appearance
-):
+
+def test_not_dot_value_sep_single_column(not_dot_value):
     """
-    Test function where `.value` is in the `names_to` argument,
-    names_pattern is used,
-    and no `index` is supplied.
+    Test output when names_sep and no dot_value
+    for a single column.
     """
-    result = df_in.pivot_longer(
-        names_to=names_to,
-        names_pattern=names_pattern,
-        sort_by_appearance=sort_by_appearance,
+
+    A = not_dot_value.loc[:, ["country", "vault_2012"]]
+    result = A.pivot_longer(
+        "country",
+        names_to=("event", "year"),
+        names_sep="_",
+        values_to="score",
     )
-    assert_frame_equal(result, df_out)
-
-
-names_single_value = [
-    (
-        pd.DataFrame(
-            {
-                "event": [1, 2],
-                "url_1": ["g1", "g3"],
-                "name_1": ["dc", "nyc"],
-                "url_2": ["g2", "g4"],
-                "name_2": ["sf", "la"],
-            }
-        ),
-        pd.DataFrame(
-            {
-                "event": [1, 1, 2, 2],
-                "url": ["g1", "g2", "g3", "g4"],
-                "name": ["dc", "sf", "nyc", "la"],
-            }
-        ),
-        "event",
-        "(.+)_.",
-        True,
-        True,
-    ),
-    (
-        pd.DataFrame(
-            {
-                "id": [1, 2, 3],
-                "x1": [4, 5, 6],
-                "x2": [5, 6, 7],
-                "y1": [7, 8, 9],
-                "y2": [10, 11, 12],
-            }
-        ),
-        pd.DataFrame(
-            {
-                "id": [1, 1, 2, 2, 3, 3],
-                "x": [4, 5, 5, 6, 6, 7],
-                "y": [7, 10, 8, 11, 9, 12],
-            },
-        ),
-        "id",
-        "(.).",
-        True,
-        True,
-    ),
-    (
-        pd.DataFrame(
-            {
-                "x1": [4, 5, 6],
-                "x2": [5, 6, 7],
-                "y1": [7, 8, 9],
-                "y2": [10, 11, 12],
-            }
-        ),
-        pd.DataFrame({"x": [4, 5, 5, 6, 6, 7], "y": [7, 10, 8, 11, 9, 12]}),
-        None,
-        "(.).",
-        True,
-        True,
-    ),
-    (
-        pd.DataFrame(
-            {
-                "x1": [4, 5, 6],
-                "x2": [5, 6, 7],
-                "y1": [7, 8, 9],
-                "y2": [10, 11, 12],
-            }
-        ),
-        pd.DataFrame(
-            {"x": [4, 5, 5, 6, 6, 7], "y": [7, 10, 8, 11, 9, 12]},
-            index=[0, 0, 1, 1, 2, 2],
-        ),
-        None,
-        "(.).",
-        False,
-        True,
-    ),
-]
-
-
-@pytest.mark.parametrize(
-    """
-    df_in,df_out,index, names_pattern,
-    ignore_index,sort_by_appearance
-    """,
-    names_single_value,
-)
-def test_single_value(
-    df_in,
-    df_out,
-    index,
-    names_pattern,
-    ignore_index,
-    sort_by_appearance,
-):
-    """Test function where names_to is a string and == `.value`."""
-    result = df_in.pivot_longer(
-        index=index,
-        names_to=".value",
-        names_pattern=names_pattern,
-        ignore_index=ignore_index,
-        sort_by_appearance=sort_by_appearance,
+    result = result.sort_values(
+        ["country", "event", "year"], ignore_index=True
     )
-    assert_frame_equal(result, df_out)
-
-
-single_column_names_pattern = [
-    (
-        pd.DataFrame(
-            {
-                "sepal_length": [5.1, 7.0],
-                "sepal_width": [3.5, 3.2],
-                "petal_length": [1.4, 4.7],
-                "petal_width": [0.2, 1.4],
-                "species": ["setosa", "versicolor"],
-            }
-        ),
-        pd.DataFrame(
-            [
-                {"species": "setosa", "part": "sepal", "value": 5.1},
-                {"species": "versicolor", "part": "sepal", "value": 7.0},
-                {"species": "setosa", "part": "sepal", "value": 3.5},
-                {"species": "versicolor", "part": "sepal", "value": 3.2},
-                {"species": "setosa", "part": "petal", "value": 1.4},
-                {"species": "versicolor", "part": "petal", "value": 4.7},
-                {"species": "setosa", "part": "petal", "value": 0.2},
-                {"species": "versicolor", "part": "petal", "value": 1.4},
-            ]
-        ),
-        "species",
-        None,
-        "part",
-        r"(.*)[_].*",
-        False,
-    ),
-    (
-        pd.DataFrame(
-            {
-                "sepal_length": [5.1, 7.0],
-                "sepal_width": [3.5, 3.2],
-                "petal_length": [1.4, 4.7],
-                "petal_width": [0.2, 1.4],
-                "species": ["setosa", "versicolor"],
-                "jargon": ["blabla", "blacksheep"],
-            }
-        ),
-        pd.DataFrame(
-            [
-                {"species": "setosa", "part": "sepal", "value": 5.1},
-                {"species": "setosa", "part": "sepal", "value": 3.5},
-                {"species": "setosa", "part": "petal", "value": 1.4},
-                {"species": "setosa", "part": "petal", "value": 0.2},
-                {"species": "versicolor", "part": "sepal", "value": 7.0},
-                {"species": "versicolor", "part": "sepal", "value": 3.2},
-                {"species": "versicolor", "part": "petal", "value": 4.7},
-                {"species": "versicolor", "part": "petal", "value": 1.4},
-            ]
-        ),
-        "species",
-        re.compile("_"),
-        "part",
-        r"(.*)[_].*",
-        True,
-    ),
-]
-
-
-@pytest.mark.parametrize(
-    """
-    df_in,df_out,index,column_names,names_to,
-    names_pattern,sort_by_appearance
-    """,
-    single_column_names_pattern,
-)
-def test_single_column_names_pattern(
-    df_in,
-    df_out,
-    index,
-    column_names,
-    names_to,
-    names_pattern,
-    sort_by_appearance,
-):
-    """
-    Test output if `names_to` is a string and
-    `names_pattern` returns a single column.
-    Also tests when both `index` and `column_names`
-    are supplied, and only a subset of the dataframe
-    is transformed.
-    """
-    result = df_in.pivot_longer(
-        index=index,
-        column_names=column_names,
-        names_to=names_to,
-        names_pattern=names_pattern,
-        sort_by_appearance=sort_by_appearance,
+    actual = A.set_index("country")
+    actual.columns = actual.columns.str.split("_", expand=True)
+    actual.columns.names = ["event", "year"]
+    actual = (
+        actual.stack(["event", "year"])
+        .rename("score")
+        .sort_index()
+        .reset_index()
     )
 
-    assert_frame_equal(result, df_out)
+    assert_frame_equal(result, actual)
 
 
-# not relevant anymore;
-# leaving it though
-# the more tests, the merrier
-def test_group_present():
-    """Test output if 'group' is in `names_to`."""
-    df_in = pd.DataFrame(
+def test_multiple_dot_value():
+    """Test output for multiple .value."""
+    df = pd.DataFrame(
         {
-            "id": [1, 2, 3],
-            "x1": [4, 5, 6],
-            "x2": [5, 6, 7],
-            "y1": [7, 8, 9],
-            "y2": [10, 11, 12],
+            "x_1_mean": [1, 2, 3, 4],
+            "x_2_mean": [1, 1, 0, 0],
+            "x_1_sd": [0, 1, 1, 1],
+            "x_2_sd": [0.739, 0.219, 1.46, 0.918],
+            "y_1_mean": [1, 2, 3, 4],
+            "y_2_mean": [1, 1, 0, 0],
+            "y_1_sd": [0, 1, 1, 1],
+            "y_2_sd": [-0.525, 0.623, -0.705, 0.662],
+            "unit": [1, 2, 3, 4],
         }
     )
 
-    df_out = pd.DataFrame(
+    result = df.pivot_longer(
+        index="unit",
+        names_to=(".value", "time", ".value"),
+        names_pattern=r"(x|y)_([0-9])(_mean|_sd)",
+    ).astype({"time": int})
+
+    actual = df.set_index("unit")
+    cols = [ent.split("_") for ent in actual.columns]
+    actual.columns = [f"{start}_{end}{middle}" for start, middle, end in cols]
+    actual = (
+        pd.wide_to_long(
+            actual.reset_index(),
+            stubnames=["x_mean", "y_mean", "x_sd", "y_sd"],
+            i="unit",
+            j="time",
+        )
+        .sort_index(axis=1)
+        .reset_index()
+    )
+
+    assert_frame_equal(result, actual)
+
+
+@pytest.fixture
+def single_val():
+    """fixture dataframe"""
+    return pd.DataFrame(
         {
-            "id": [1, 2, 3, 1, 2, 3],
-            "group": ["1", "1", "1", "2", "2", "2"],
-            "x": [4, 5, 6, 5, 6, 7],
-            "y": [7, 8, 9, 10, 11, 12],
+            "id": [1, 2, 3],
+            "x1": [4, 5, 6],
+            "x2": [5, 6, 7],
         }
     )
 
-    result = df_in.pivot_longer(
-        index="id",
-        names_to=[".value", "group"],
-        names_pattern="(.)(.)",
+
+def test_multiple_dot_value2(single_val):
+    """Test output for multiple .value."""
+
+    result = single_val.pivot_longer(
+        index="id", names_to=(".value", ".value"), names_pattern="(.)(.)"
     )
 
-    assert_frame_equal(result, df_out)
+    assert_frame_equal(result, single_val)
 
 
-# copied from :
-# https://github.com/pandas-dev/pandas/blob/master/pandas/tests/reshape/test_melt.py
-def test_float_suffix_irregular():
+def test_names_pattern_sequence_single_unique_column(single_val):
     """
-    Test output for floating suffixes for stubnames;
-    the suffixes are unique and differ for the stubnames.
+    Test output if names_pattern is a sequence of length 1.
     """
-    df = pd.DataFrame(
-        {
-            "treatment_1.1": [1.0, 2.0],
-            "treatment_2.1": [3.0, 4.0],
-            "result_1.2": [5.0, 6.0],
-            "result_1": [0, 9],
-            "A": ["X1", "X2"],
-        }
-    )
 
-    expected = pd.DataFrame(
-        {
-            "A": ["X1", "X1", "X1", "X1", "X2", "X2", "X2", "X2"],
-            "colname": ["1.1", "2.1", "1.2", "1", "1.1", "2.1", "1.2", "1"],
-            "treatment": [1.0, 3.0, np.nan, np.nan, 2.0, 4.0, np.nan, np.nan],
-            "result": [np.nan, np.nan, 5.0, 0.0, np.nan, np.nan, 6.0, 9.0],
-        }
+    result = single_val.pivot_longer(
+        "id", names_to=["x"], names_pattern=("x",)
     )
-
-    result = df.pivot_longer(
-        index="A",
-        names_to=(".value", "colname"),
-        names_sep="_",
-        sort_by_appearance=True,
+    actual = (
+        pd.wide_to_long(single_val, ["x"], i="id", j="num")
+        .droplevel("num")
+        .reset_index()
     )
-    assert_frame_equal(result, expected)
-
-
-multiple_column_names = [
-    (
-        pd.DataFrame(
-            {
-                "Sony | TV | Model | value": {
-                    0: "A222",
-                    1: "A234",
-                    2: "A4345",
-                },
-                "Sony | TV | Quantity | value": {0: 5, 1: 5, 2: 4},
-                "Sony | TV | Max-quant | value": {0: 10, 1: 9, 2: 9},
-                "Panasonic | TV | Model | value": {
-                    0: "T232",
-                    1: "S3424",
-                    2: "X3421",
-                },
-                "Panasonic | TV | Quantity | value": {0: 1, 1: 5, 2: 1},
-                "Panasonic | TV | Max-quant | value": {0: 10, 1: 12, 2: 11},
-                "Sanyo | Radio | Model | value": {
-                    0: "S111",
-                    1: "S1s1",
-                    2: "S1s2",
-                },
-                "Sanyo | Radio | Quantity | value": {0: 4, 1: 2, 2: 4},
-                "Sanyo | Radio | Max-quant | value": {0: 9, 1: 9, 2: 10},
-            }
-        ),
-        pd.DataFrame(
-            [
-                {
-                    "Manufacturer": "Sony ",
-                    "Device": " TV ",
-                    " Model ": "A222",
-                    " Quantity ": 5,
-                    " Max-quant ": 10,
-                },
-                {
-                    "Manufacturer": "Sony ",
-                    "Device": " TV ",
-                    " Model ": "A234",
-                    " Quantity ": 5,
-                    " Max-quant ": 9,
-                },
-                {
-                    "Manufacturer": "Sony ",
-                    "Device": " TV ",
-                    " Model ": "A4345",
-                    " Quantity ": 4,
-                    " Max-quant ": 9,
-                },
-                {
-                    "Manufacturer": "Panasonic ",
-                    "Device": " TV ",
-                    " Model ": "T232",
-                    " Quantity ": 1,
-                    " Max-quant ": 10,
-                },
-                {
-                    "Manufacturer": "Panasonic ",
-                    "Device": " TV ",
-                    " Model ": "S3424",
-                    " Quantity ": 5,
-                    " Max-quant ": 12,
-                },
-                {
-                    "Manufacturer": "Panasonic ",
-                    "Device": " TV ",
-                    " Model ": "X3421",
-                    " Quantity ": 1,
-                    " Max-quant ": 11,
-                },
-                {
-                    "Manufacturer": "Sanyo ",
-                    "Device": " Radio ",
-                    " Model ": "S111",
-                    " Quantity ": 4,
-                    " Max-quant ": 9,
-                },
-                {
-                    "Manufacturer": "Sanyo ",
-                    "Device": " Radio ",
-                    " Model ": "S1s1",
-                    " Quantity ": 2,
-                    " Max-quant ": 9,
-                },
-                {
-                    "Manufacturer": "Sanyo ",
-                    "Device": " Radio ",
-                    " Model ": "S1s2",
-                    " Quantity ": 4,
-                    " Max-quant ": 10,
-                },
-            ]
-        ),
-    ),
-    (
-        pd.DataFrame(
-            {
-                "Sony | TV | Model | value": {
-                    0: "A222",
-                    1: "A234",
-                    2: "A4345",
-                },
-                "Sony | TV | Quantity | value": {0: 5, 1: 5, 2: 4},
-                "Sony | TV | Max-quant | value": {0: 10, 1: 9, 2: 9},
-                "Panasonic | TV | Model | value": {
-                    0: "T232",
-                    1: "S3424",
-                    2: "X3421",
-                },
-                "Panasonic | TV | Quantity | value": {0: 1, 1: 5, 2: 1},
-                "Panasonic | TV | Max-quant | value": {0: 10, 1: 12, 2: 11},
-                "Sanyo | Radio | Model | value": {
-                    0: "S111",
-                    1: "S1s1",
-                    2: "S1s2",
-                },
-                "Sanyo | Radio | Quantity | value": {0: 4, 1: 2, 2: 4},
-                "Sanyo | radio | Max-quant | value": {0: 9, 1: 9, 2: 10},
-            }
-        ),
-        pd.DataFrame(
-            [
-                {
-                    "Manufacturer": "Sony ",
-                    "Device": " TV ",
-                    " Model ": "A222",
-                    " Quantity ": 5.0,
-                    " Max-quant ": 10.0,
-                },
-                {
-                    "Manufacturer": "Sony ",
-                    "Device": " TV ",
-                    " Model ": "A234",
-                    " Quantity ": 5.0,
-                    " Max-quant ": 9.0,
-                },
-                {
-                    "Manufacturer": "Sony ",
-                    "Device": " TV ",
-                    " Model ": "A4345",
-                    " Quantity ": 4.0,
-                    " Max-quant ": 9.0,
-                },
-                {
-                    "Manufacturer": "Panasonic ",
-                    "Device": " TV ",
-                    " Model ": "T232",
-                    " Quantity ": 1.0,
-                    " Max-quant ": 10.0,
-                },
-                {
-                    "Manufacturer": "Panasonic ",
-                    "Device": " TV ",
-                    " Model ": "S3424",
-                    " Quantity ": 5.0,
-                    " Max-quant ": 12.0,
-                },
-                {
-                    "Manufacturer": "Panasonic ",
-                    "Device": " TV ",
-                    " Model ": "X3421",
-                    " Quantity ": 1.0,
-                    " Max-quant ": 11.0,
-                },
-                {
-                    "Manufacturer": "Sanyo ",
-                    "Device": " Radio ",
-                    " Model ": "S111",
-                    " Quantity ": 4.0,
-                    " Max-quant ": np.nan,
-                },
-                {
-                    "Manufacturer": "Sanyo ",
-                    "Device": " Radio ",
-                    " Model ": "S1s1",
-                    " Quantity ": 2.0,
-                    " Max-quant ": np.nan,
-                },
-                {
-                    "Manufacturer": "Sanyo ",
-                    "Device": " Radio ",
-                    " Model ": "S1s2",
-                    " Quantity ": 4.0,
-                    " Max-quant ": np.nan,
-                },
-                {
-                    "Manufacturer": "Sanyo ",
-                    "Device": " radio ",
-                    " Model ": np.nan,
-                    " Quantity ": np.nan,
-                    " Max-quant ": 9.0,
-                },
-                {
-                    "Manufacturer": "Sanyo ",
-                    "Device": " radio ",
-                    " Model ": np.nan,
-                    " Quantity ": np.nan,
-                    " Max-quant ": 9.0,
-                },
-                {
-                    "Manufacturer": "Sanyo ",
-                    "Device": " radio ",
-                    " Model ": np.nan,
-                    " Quantity ": np.nan,
-                    " Max-quant ": 10.0,
-                },
-            ]
-        ),
-    ),
-]
-
-
-@pytest.mark.parametrize("df_in, df_out", multiple_column_names)
-def test_multiple_column_names(df_in, df_out):
+
+    assert_frame_equal(result, actual)
+
+
+def test_names_pattern_single_column(single_val):
     """
-    Test output for scenario where the pairing
-    in the column name is more than 2.
+    Test output if names_to is only '.value'.
     """
 
-    result = df_in.pivot_longer(
-        names_to=("Manufacturer", "Device", ".value"),
-        names_pattern=r"(.+)\|(.+)\|(.+)\|.*",
+    result = single_val.pivot_longer(
+        "id", names_to=".value", names_pattern="(.)."
+    )
+    actual = (
+        pd.wide_to_long(single_val, ["x"], i="id", j="num")
+        .droplevel("num")
+        .reset_index()
     )
 
-    assert_frame_equal(result, df_out)
+    assert_frame_equal(result, actual)
 
 
-def test_names_sep_non_unique_columns():
-    """Test output for non_unique columns with names_sep"""
+def test_names_pattern_single_column_not_dot_value(single_val):
+    """
+    Test output if names_to is not '.value'.
+    """
+    df = single_val[["x1"]]
+    result = df.pivot_longer(names_to="yA", names_pattern="(.+)")
 
-    df = pd.DataFrame(
-        {
-            "event": [1, 2],
-            "url_1": ["g1", "g3"],
-            "name_1": ["dc", "nyc"],
-            "url_2": ["g2", "g4"],
-            "name_2": ["sf", "la"],
-        }
-    )
-    df.columns = ["event", "url_1", "name_1", "url_1", "name_2"]
-
-    expected = pd.DataFrame(
-        [
-            {"event": 1, "num": "1", "url": "g1", "name": "dc"},
-            {"event": 2, "num": "1", "url": "g3", "name": "nyc"},
-            {"event": 1, "num": "1", "url": "g2", "name": np.nan},
-            {"event": 2, "num": "1", "url": "g4", "name": np.nan},
-            {"event": 1, "num": "2", "url": np.nan, "name": "sf"},
-            {"event": 2, "num": "2", "url": np.nan, "name": "la"},
-        ]
-    )
+    assert_frame_equal(result, df.melt(var_name="yA"))
 
-    result = df.pivot_longer(
-        "event", names_to=(".value", "num"), names_sep="_", ignore_index=True
-    )
-    assert_frame_equal(result, expected)
+
+def test_names_pattern_single_column_not_dot_value1(single_val):
+    """
+    Test output if names_to is not '.value'.
+    """
+    df = single_val[["id", "x1"]]
+    result = df.pivot_longer(index="id", names_to="yA", names_pattern="(.+)")
+
+    assert_frame_equal(result, df.melt("id", var_name="yA"))
 
 
-def test_names_pattern_sequence_single_unique_column():
+def test_names_pattern_seq_single_column(single_val):
     """
-    Test output if names_pattern is a sequence,
-    and there is just one unique column.
+    Test output if names_pattern is a list.
     """
+    df = single_val[["id", "x1"]]
+    result = df.pivot_longer(index="id", names_to="yA", names_pattern=["(.+)"])
+
+    assert_frame_equal(result, df.rename(columns={"x1": "yA"}))
+
+
+def test_names_pattern_nulls_in_data():
+    """Test output if nulls are present in data."""
     df = pd.DataFrame(
         {
-            "id": [1, 2, 3],
-            "x1": [4, 5, 6],
-            "x2": [5, 6, 7],
+            "family": [1, 2, 3, 4, 5],
+            "dob_child1": [
+                "1998-11-26",
+                "1996-06-22",
+                "2002-07-11",
+                "2004-10-10",
+                "2000-12-05",
+            ],
+            "dob_child2": [
+                "2000-01-29",
+                np.nan,
+                "2004-04-05",
+                "2009-08-27",
+                "2005-02-28",
+            ],
+            "gender_child1": [1, 2, 2, 1, 2],
+            "gender_child2": [2.0, np.nan, 2.0, 1.0, 1.0],
         }
     )
 
-    result = df.pivot_longer("id", names_to=["x"], names_pattern=("x",))
-    expected = pd.DataFrame(
-        [
-            {"id": 1, "x": 4},
-            {"id": 2, "x": 5},
-            {"id": 3, "x": 6},
-            {"id": 1, "x": 5},
-            {"id": 2, "x": 6},
-            {"id": 3, "x": 7},
-        ]
+    result = df.pivot_longer(
+        "family",
+        names_to=[".value", "child"],
+        names_pattern=r"(.+)_(.+)\d",
+        ignore_index=False,
+    )
+    result.index = range(len(result))
+
+    actual = (
+        pd.wide_to_long(
+            df, ["dob", "gender"], i="family", j="child", sep="_", suffix=".+"
+        )
+        .reset_index()
+        .assign(child=lambda df: df.child.str[:-1])
     )
 
-    assert_frame_equal(result, expected)
+    assert_frame_equal(result, actual)