Repository: pyjanitor-devs/pyjanitor

Issue:
[BUG] Weird output with pivot_longer
# Brief Description

`pivot_longer` outputs wrong results when a sequence is used in `names_pattern`

# System Information

<!-- System information helps us. To keep things simple, just let us know the OS and Python version first.
You can provide the optional information later. -->

- Operating system:Linux
- Python version (required): 3.9

# Minimally Reproducible Code

```python
import pandas as pd
import numpy as np
import janitor as jn

df = pd.DataFrame(
            {
                "id": [1, 2, 3],
                "x1": [4, 5, 6],
                "x2": [5, 6, 7],
                "y1": [7, 8, 9],
                "y2": [10, 11, 12],
            }
        )

df

   id  x1  x2  y1  y2
0   1   4   5   7  10
1   2   5   6   8  11
2   3   6   7   9  12


# works fine when there are multiple values in names_pattern
In [5]: df.pivot_longer('id', names_to=('x', 'y'), names_pattern=('x', 'y'))
Out[5]: 
   id  x   y
0   1  4   7
1   2  5   8
2   3  6   9
3   1  5  10
4   2  6  11
5   3  7  12

# Wrong output for a single value

In [6]: df = df.iloc[:, :3]

In [8]: df
Out[8]: 
   id  x1  x2
0   1   4   5
1   2   5   6
2   3   6   7


In [7]: df.pivot_longer('id', names_to=('x',), names_pattern=('x',)) # where did x go?
Out[7]: 
   id
0   1
1   2
2   3
3   1
4   2
5   3
```


Discussion:

Resolving patch:
diff --git a/janitor/functions.py b/janitor/functions.py
--- a/janitor/functions.py
+++ b/janitor/functions.py
@@ -5745,7 +5745,7 @@ def pivot_longer(
     df: pd.DataFrame,
     index: Optional[Union[List, Tuple, str, Pattern]] = None,
     column_names: Optional[Union[List, Tuple, str, Pattern]] = None,
-    names_to: Optional[Union[List, Tuple, str]] = "variable",
+    names_to: Optional[Union[List, Tuple, str]] = None,
     values_to: Optional[str] = "value",
     column_level: Optional[Union[int, str]] = None,
     names_sep: Optional[Union[str, Pattern]] = None,
@@ -6134,8 +6134,7 @@ def pivot_longer(
         also be a list/tuple of strings that will serve as new column
         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 `values_to` will be
-        replaced.
+        from part of the existing column names and overrides`values_to`.
     :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
@@ -6175,24 +6174,8 @@ def pivot_longer(
         format.
     :raises TypeError: if `index` or `column_names` is not a string, or a
         list/tuple of column names, or a `janitor.patterns` function.
-    :raises TypeError: if `names_to` or `column_names` is not a string, or a
-        list/tuple of strings.
-    :raises TypeError: if `names_sep` is not a string or regular expression.
-    :raises TypeError: if `names_pattern` is not a regular expression, or a
-        list/tuple of regular expressions.
-    :raises TypeError: if `values_to` is not a string.
-    :raises TypeError: if `sort_by_appearance` is not a boolean.
-    :raises TypeError: if `ignore_index` is not a boolean.
-    :raises ValueError: if `names_to` is a string or a list/tuple of length 1,
-        and `names_sep` is provided.
-    :raises ValueError: if `names_to` is a string, and the number of extracted
-        columns is greater than 1.
-    :raises ValueError: if `names_to` is a list/tuple, and its length does not
-        match the number of extracted columns.
     :raises ValueError: if the dataframe contains MultiIndex columns, and
         `index` or `column_names` is not a list of tuples.
-    :raises ValueError: if the dataframe contains MultiIndex columns, and
-        either `names_sep` or `names_pattern` is provided.
 
     .. # noqa: DAR402
     """
diff --git a/janitor/utils.py b/janitor/utils.py
--- a/janitor/utils.py
+++ b/janitor/utils.py
@@ -2,13 +2,14 @@
 
 import fnmatch
 import functools
+from collections import defaultdict
 import os
 import re
 import socket
 import sys
 import warnings
 from collections.abc import Callable as dispatch_callable
-from itertools import chain, combinations
+from itertools import chain
 from typing import (
     Callable,
     Dict,
@@ -668,7 +669,6 @@ def _sub_expand_grid(  # noqa: F811
             value.name = f"{key}_{value.name}"
         else:
             value.name = key
-
     return value
 
 
@@ -1068,97 +1068,140 @@ def _data_checks_pivot_longer(
             column_names = list(column_names)
         column_names = _select_columns(column_names, df)
 
-    if isinstance(names_to, str):
-        names_to = [names_to]
+    len_names_to = 0
+    if names_to is not None:
+        if isinstance(names_to, str):
+            names_to = [names_to]
+        elif isinstance(names_to, tuple):
+            names_to = list(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__}
+                    """
+                )
 
-    elif isinstance(names_to, tuple):
-        names_to = list(names_to)
+            if word in unique_names_to:
+                raise ValueError(
+                    f"""
+                    {word} already exists in names_to.
+                    Duplicates are not allowed.
+                    """
+                )
+            unique_names_to.add(word)  # noqa: PD005
+        unique_names_to = None
 
-    check("names_to", names_to, [list])
+        len_names_to = len(names_to)
 
-    if not all((isinstance(word, str) for word in names_to)):
-        raise TypeError("All entries in `names_to` argument must be strings.")
+    if names_sep and names_pattern:
+        raise ValueError(
+            """
+                Only one of names_pattern or names_sep
+                should be provided.
+                """
+        )
 
-    if len(names_to) > 1:
-        if all((names_pattern, names_sep)):
+    if names_pattern is not None:
+        check("names_pattern", names_pattern, [str, Pattern, list, tuple])
+        if names_to is None:
             raise ValueError(
                 """
-                Only one of `names_pattern` or `names_sep`
-                should be provided.
+                Kindly provide values for names_to.
                 """
             )
+        if isinstance(names_pattern, (str, Pattern)):
+            num_regex_grps = re.compile(names_pattern).groups
 
-        if (".value" in names_to) and (names_to.count(".value") > 1):
-            raise ValueError("There can be only one `.value` in `names_to`.")
-
-    # names_sep creates more than one column
-    # whereas regex with names_pattern can be limited to one column
-    if (len(names_to) == 1) and (names_sep is not None):
-        raise ValueError(
-            """
-            For a single `names_to` value,
-            `names_sep` is not required.
-            """
-        )
-    if names_pattern is not None:
-        check("names_pattern", names_pattern, [str, Pattern, List, Tuple])
-
-        if isinstance(names_pattern, (list, tuple)):
-            if not all(
-                isinstance(word, (str, Pattern)) for word in names_pattern
-            ):
-                raise TypeError(
-                    """
-                    All entries in the ``names_pattern`` argument
-                    must be regular expressions.
+            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}
                     """
                 )
 
-            if len(names_pattern) != len(names_to):
+        if isinstance(names_pattern, (list, tuple)):
+            for word in names_pattern:
+                if not isinstance(word, (str, Pattern)):
+                    raise TypeError(
+                        f"""
+                        All entries in the names_pattern argument
+                        must be regular expressions.
+                        `{word}` is of type {type(word).__name__}
+                        """
+                    )
+
+            if len(names_pattern) != len_names_to:
                 raise ValueError(
-                    """
-                    Length of ``names_to`` does not match
-                    number of patterns.
+                    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)}
                     """
                 )
 
-            if ".value" in names_to:
+            if names_to and (".value" in names_to):
                 raise ValueError(
                     """
-                    ``.value`` is not accepted
-                    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.
+                """
+            )
 
     check("values_to", values_to, [str])
+    df_columns = df.columns
 
-    if (values_to in df.columns) and not any(
-        (
-            ".value" in names_to,
-            isinstance(names_pattern, (list, tuple)),
-        )
-    ):
+    dot_value = (names_to is not None) and (
+        (".value" in names_to) or (isinstance(names_pattern, (list, tuple)))
+    )
+    if (values_to in df_columns) and (not dot_value):
         # copied from pandas' melt source code
         # with a minor tweak
         raise ValueError(
             """
             This dataframe has a column name that matches the
-            'values_to' column name of the resulting Dataframe.
-            Kindly set the 'values_to' parameter to a unique name.
+            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.
             """
         )
 
     if any((names_sep, names_pattern)) and (
-        isinstance(df.columns, pd.MultiIndex)
+        isinstance(df_columns, pd.MultiIndex)
     ):
         raise ValueError(
             """
             Unpivoting a MultiIndex column dataframe
-            when `names_sep` or `names_pattern` is supplied
+            when names_sep or names_pattern is supplied
             is not supported.
             """
         )
@@ -1167,7 +1210,7 @@ def _data_checks_pivot_longer(
         # adapted from pandas' melt source code
         if (
             (index is not None)
-            and isinstance(df.columns, pd.MultiIndex)
+            and isinstance(df_columns, pd.MultiIndex)
             and (not isinstance(index, list))
         ):
             raise ValueError(
@@ -1179,7 +1222,7 @@ def _data_checks_pivot_longer(
 
         if (
             (column_names is not None)
-            and isinstance(df.columns, pd.MultiIndex)
+            and isinstance(df_columns, pd.MultiIndex)
             and (not isinstance(column_names, list))
         ):
             raise ValueError(
@@ -1208,285 +1251,297 @@ def _data_checks_pivot_longer(
 
 
 def _sort_by_appearance_for_melt(
-    df: pd.DataFrame, ignore_index: bool, len_index: int
+    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`.
 
-    An example for `sort_by_appearance`:
-
-    Say data looks like this :
-        id, a1, a2, a3, A1, A2, A3
-         1, a, b, c, A, B, C
-
-    when unpivoted into long form, it will look like this :
-              id instance    a     A
-        0     1     1        a     A
-        1     1     2        b     B
-        2     1     3        c     C
-
-    where the column `a` comes before `A`, as it was in the source data,
-    and in column `a`, `a > b > c`, also as it was in the source data.
-
     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'])
+
+    #   first last variable  value
+    # 0  John  Doe   height    5.5
+    # 1  Mary   Bo   height    6.0
+    # 2  John  Doe   weight  130.0
+    # 3  Mary   Bo   weight  150.0
+
+    # sort_by_appearance `untiles` the index
+    # and keeps all `John` before all `Mary`
+    # since `John` appears first in the original dataframe:
+
+    #   first last variable  value
+    # 0  John  Doe   height    5.5
+    # 1  John  Doe   weight  130.0
+    # 2  Mary   Bo   height    6.0
+    # 3  Mary   Bo   weight  150.0
+
+    # to get to this second form, which is sorted by appearance,
+    # 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.
+    # reshaping allows us to track the original positions
+    # in the previous dataframe ->
+    # np.reshape([0,1,2,3], (-1, 2))
+    # 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`
     if not length_check:
-        index_sorter = np.reshape(np.arange(len(df)), (-1, len_index)).ravel(
-            order="F"
-        )
+        index_sorter = np.arange(len(df))
+        index_sorter = np.reshape(index_sorter, (-1, len_index))
+        index_sorter = index_sorter.ravel(order="F")
         df = df.take(index_sorter)
 
-        if ignore_index:
-            df.index = np.arange(len(df))
-
     return df
 
 
-def _pivot_longer_extractions(
+def _pivot_longer_names_sep(
     df: pd.DataFrame,
-    index: Optional[Union[List, Tuple]] = None,
-    column_names: Optional[Union[List, Tuple]] = None,
-    names_to: Optional[List] = None,
-    names_sep: Optional[Union[str, Pattern]] = None,
-    names_pattern: Optional[
-        Union[
-            List[Union[str, Pattern]], Tuple[Union[str, Pattern]], str, Pattern
-        ]
-    ] = None,
-) -> Tuple:
-
+    index,
+    names_to: list,
+    names_sep: Union[str, Pattern],
+    values_to: str,
+    sort_by_appearance: bool,
+    ignore_index: bool,
+) -> pd.DataFrame:
     """
-    This is where the labels within the column names are separated
-    into new columns, and is executed if `names_sep` or `names_pattern`
-    is not None.
-
-    A dataframe is returned.
+    This takes care of pivoting scenarios where
+    names_sep is provided.
     """
 
-    if any((names_sep, names_pattern)):
-        if index:
-            df = df.set_index(index, append=True)
-
-        if column_names:
-            df = df.loc[:, column_names]
-
-    mapping = None
-    if names_sep:
-        mapping = df.columns.str.split(names_sep, expand=True)
-
-        if len(mapping.names) != len(names_to):
-            raise ValueError(
-                """
-                The length of ``names_to`` does not match
-                the number of columns extracted.
-                """
-            )
-        mapping.names = names_to
-
-    elif isinstance(names_pattern, str):
-        mapping = df.columns.str.extract(names_pattern, expand=True)
+    mapping = pd.Series(df.columns).str.split(names_sep, expand=True)
+    len_mapping_columns = len(mapping.columns)
+    len_names_to = len(names_to)
 
-        if mapping.isna().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.
-                """
-            )
+    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}.
+            """
+        )
 
-        if mapping.isna().any(axis=None):
-            raise ValueError(
-                """
-                Not all labels in the columns
-                matched the regular expression
-                in ``names_pattern``.
-                Kindly provide a regular expression
-                that matches all labels in the columns.
-                """
-            )
+    mapping.columns = names_to
 
-        if len(names_to) != len(mapping.columns):
-            raise ValueError(
-                """
-                The length of ``names_to`` does not match
-                the number of columns extracted.
-                """
-            )
+    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 len(mapping.columns) == 1:
-            mapping = pd.Index(mapping.iloc[:, 0], name=names_to[0])
-        else:
-            mapping = pd.MultiIndex.from_frame(mapping, names=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.
+    # This is relevant only for scenarios where `.value` is
+    # in names_to.
+    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
 
-    elif isinstance(names_pattern, (list, tuple)):
-        mapping = [
-            df.columns.str.contains(regex, na=False) for regex in names_pattern
-        ]
+    return _pivot_longer_frame_MultiIndex(
+        df, index, sort_by_appearance, ignore_index, values_to
+    )
 
-        if not np.any(mapping):
-            raise ValueError(
-                """
-                Not all labels in the columns
-                matched the regular expression
-                in ``names_pattern``.
-                Kindly provide a regular expression
-                that matches all labels in the columns.
-                """
-            )
 
-        mapping = np.select(mapping, names_to, None)
-        mapping = pd.Index(mapping, name=".value")
+def _pivot_longer_names_pattern_str(
+    df: pd.DataFrame,
+    index,
+    names_to: list,
+    names_pattern: Union[str, Pattern],
+    values_to: str,
+    sort_by_appearance: bool,
+    ignore_index: bool,
+) -> pd.DataFrame:
+    """
+    This takes care of pivoting scenarios where
+    names_pattern is provided, and is a string.
+    """
 
-        if np.any(mapping.isna()):
-            raise ValueError(
-                """
-                The regular expressions in ``names_pattern``
-                did not return all matches.
-                Kindly provide a regular expression that
-                captures all patterns.
-                """
-            )
+    mapping = df.columns.str.extract(names_pattern, expand=True)
 
-    outcome = None
-    single_index_mapping = not isinstance(mapping, pd.MultiIndex)
-    if single_index_mapping:
-        outcome = pd.Series(mapping)
-        outcome = outcome.groupby(outcome).cumcount()
-        mapping = pd.MultiIndex.from_arrays([mapping, outcome])
-        outcome = None
+    nulls_found = mapping.isna()
 
-    df.columns = mapping
+    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.
+            """
+        )
 
-    dot_value = any(
-        ((".value" in names_to), isinstance(names_pattern, (list, tuple)))
-    )
+    if nulls_found.any(axis=None):
+        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.
+            """
+        )
 
-    first = None
-    last = None
-    complete_index = None
-    dtypes = None
-    cumcount = None
-    if dot_value:
-        if not mapping.is_unique:
-            cumcount = pd.factorize(mapping)[0]
-            cumcount = pd.Series(cumcount).groupby(cumcount).cumcount()
-        cumcount_check = cumcount is not None
-        mapping_names = mapping.names
-        mapping = [mapping.get_level_values(name) for name in mapping_names]
-        dtypes = [
-            CategoricalDtype(categories=entry.unique(), ordered=True)
-            for entry in mapping
-        ]
-        mapping = [
-            entry.astype(dtype) for entry, dtype in zip(mapping, dtypes)
-        ]
+    mapping.columns = names_to
 
-        if cumcount_check:
-            mapping.append(cumcount)
-        mapping = pd.MultiIndex.from_arrays(mapping)
+    if len(names_to) == 1:
+        mapping = mapping.squeeze()
         df.columns = mapping
-
-        mapping = df.columns
-        if cumcount_check:
-            mapping = mapping.droplevel(-1)
-
-        # test if all combinations are present
-        first = mapping.get_level_values(".value")
-        last = mapping.droplevel(".value")
-        outcome = first.groupby(last)
-        outcome = (value for _, value in outcome.items())
-        outcome = combinations(outcome, 2)
-        outcome = (
-            left.symmetric_difference(right).empty for left, right in outcome
+        return _pivot_longer_frame_single_Index(
+            df, index, sort_by_appearance, ignore_index, values_to
         )
 
-        # include all combinations into the columns
-        if not all(outcome):
-            if isinstance(last, pd.MultiIndex):
-                indexer = (first.drop_duplicates(), last.drop_duplicates())
-                complete_index = _complete_indexer_expand_grid(indexer)
-                complete_index = complete_index.reorder_levels(
-                    [*mapping.names]
+    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.
+                    """
                 )
 
-            else:
-                complete_index = pd.MultiIndex.from_product(mapping.levels)
+    mapping_is_unique = not mapping.duplicated().any(axis=None).item()
 
-            df = df.reindex(columns=complete_index)
-        if cumcount_check:
-            df = df.droplevel(-1, axis=1)
+    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 df, single_index_mapping
+    return _pivot_longer_frame_MultiIndex(
+        df, index, sort_by_appearance, ignore_index, values_to
+    )
 
 
-def _computations_pivot_longer(
+def _pivot_longer_names_pattern_sequence(
     df: pd.DataFrame,
-    index: Optional[Union[List, Tuple]] = None,
-    column_names: Optional[Union[List, Tuple]] = None,
-    names_to: Optional[Union[List, Tuple, str]] = None,
-    values_to: Optional[str] = "value",
-    column_level: Optional[Union[int, str]] = None,
-    names_sep: Optional[Union[str, Pattern]] = None,
-    names_pattern: Optional[
-        Union[
-            List[Union[str, Pattern]], Tuple[Union[str, Pattern]], str, Pattern
-        ]
-    ] = None,
-    sort_by_appearance: Optional[bool] = False,
-    ignore_index: Optional[bool] = True,
+    index,
+    names_to: list,
+    names_pattern: Union[list, tuple],
+    sort_by_appearance: bool,
+    ignore_index: bool,
 ) -> pd.DataFrame:
     """
-    This is the main workhorse of the `pivot_longer` function.
-    Below is a summary of how the function accomplishes its tasks:
-
-    1. If `names_sep` or `names_pattern` is not provided, then regular data
-       unpivoting is covered with pandas melt.
-
-    2. If `names_sep` or `names_pattern` is not None, the first step is to
-       extract the relevant values from the columns, using either
-       `str.split(expand=True)`, if `names_sep` is provided, or `str.extract()`
-       if `names_pattern` is provided. If `names_pattern` is a list/tuple of
-       regular expressions, then `str.contains` along with `numpy` select is
-       used for the extraction.
+    This takes care of pivoting scenarios where
+    names_pattern is provided, and is a list/tuple.
+    """
 
-        After the extraction, `pd.melt` is executed.
+    df_columns = df.columns
+    mapping = [
+        df_columns.str.contains(regex, na=False, regex=True)
+        for regex in names_pattern
+    ]
 
-    3. 'The labels in `names_to` become the new column names, if `.value`
-        is not in `names_to`, or if `names_pattern` is not a list/tuple of
-        regexes.
+    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.
+            """
+        )
+    for position, boolean in enumerate(matches):
+        if boolean.item() is False:
+            raise ValueError(
+                f"""
+                No match was returned for
+                regex `{names_pattern[position]}`
+                """
+            )
 
-    4.  If, however, `names_to` contains `.value`, or `names_pattern` is a
-        list/tuple of regexes, then the `.value` column is unstacked(in a
-        manner of speaking, `pd.DataFrame.unstack` is not actually used) to
-        become new column name(s), while the other values, if any, go under
-        different column names. `values_to` is overriden.
+    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")
 
-    5.  If `ignore_index` is `False`, then the index of the source dataframe is
-        returned, and repeated as necessary.
+    return _pivot_longer_frame_single_Index(
+        df, index, sort_by_appearance, ignore_index, values_to=None
+    )
 
-    6.  If the user wants the data in order of appearance, in which case, the
-        unpivoted data appears in stacked form, then `sort_by_appearance`
-        covers that.
 
-    An unpivoted dataframe is returned.
+def _computations_pivot_longer(
+    df: pd.DataFrame,
+    index: list = None,
+    column_names: list = None,
+    names_to: list = None,
+    values_to: str = "value",
+    column_level: Union[int, str] = None,
+    names_sep: Union[str, Pattern] = None,
+    names_pattern: Union[list, tuple, str, Pattern] = None,
+    sort_by_appearance: bool = False,
+    ignore_index: bool = True,
+) -> pd.DataFrame:
+    """
+    This is where the final dataframe in long form is created.
     """
 
     if (
         (index is None)
         and column_names
-        and (len(df.columns) > len(column_names))
+        and (df.columns.size > len(column_names))
     ):
         index = [
             column_name
@@ -1494,10 +1549,21 @@ def _computations_pivot_longer(
             if column_name not in column_names
         ]
 
-    len_index = len(df)
-
     # scenario 1
     if all((names_pattern is None, names_sep is None)):
+        if names_to:
+            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.
+                        """
+                    )
+
+        len_index = len(df)
 
         df = pd.melt(
             df,
@@ -1510,83 +1576,256 @@ def _computations_pivot_longer(
         )
 
         if sort_by_appearance:
-            df = _sort_by_appearance_for_melt(
-                df=df, ignore_index=ignore_index, len_index=len_index
-            )
+            df = _sort_by_appearance_for_melt(df=df, len_index=len_index)
+
+        if ignore_index:
+            df.index = np.arange(len(df))
 
         return df
 
-    df, single_index_mapping = _pivot_longer_extractions(
-        df=df,
-        index=index,
-        column_names=column_names,
-        names_to=names_to,
-        names_sep=names_sep,
-        names_pattern=names_pattern,
-    )
+    # names_sep or names_pattern
+    if index:
+        df = df.set_index(index, append=True)
 
-    # df_columns = df.columns
-    unique_names = None
-    drop_column = None
-    dot_value = ".value" in df.columns.names
+    if column_names:
+        df = df.loc[:, column_names]
 
-    if not dot_value:
-        if single_index_mapping:
-            unique_names = df.columns.names[0]
-            drop_column = "_".join([unique_names, values_to])
-            df.columns.names = [unique_names, drop_column]
-        df = pd.melt(
-            df, id_vars=None, value_name=values_to, ignore_index=False
-        )
+    df_index_names = df.index.names
 
-    else:
-        unique_names = df.columns.get_level_values(".value").categories
-        if single_index_mapping:
-            # passing `drop_column` to `melt` downstream
-            # avoids any name conflict with `var_name`,
-            # especially if var_name exists in the names
-            # associated with .value.
-            drop_column = "_".join(unique_names)
-        # ensures that the correct values are aligned,
-        # in preparation for the recombination
-        # of the columns downstream
-        df = df.sort_index(axis=1)
-
-        df = [
-            df.xs(key=name, level=".value", axis=1).melt(
-                ignore_index=False, var_name=drop_column, value_name=name
+    # checks to avoid duplicate columns
+    # 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.
+                """
             )
-            for name in unique_names
-        ]
 
-        first, *rest = df
+        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.
+                """
+            )
 
-        # `first` has all the required columns;
-        # as such, there is no need to keep these columns in
-        # the other dataframes in `rest`;
-        # plus, we avoid duplicate columns during concatenation
-        # the only column we need is the last column,
-        # from each dataframe in `rest`
-        # uniformity in the data is already assured
-        # with the categorical dtype creation,
-        # followed by the sorting on the columns earlier.
-        rest = [frame.iloc[:, -1] for frame in rest]
-        # df = first.join(rest, how = 'outer', sort = False)
-        df = pd.concat([first, *rest], axis=1)
+    if names_sep:
+        return _pivot_longer_names_sep(
+            df,
+            index,
+            names_to,
+            names_sep,
+            values_to,
+            sort_by_appearance,
+            ignore_index,
+        )
 
-    if single_index_mapping:
-        df = df.drop(columns=drop_column)
+    if isinstance(names_pattern, (str, Pattern)):
+        return _pivot_longer_names_pattern_str(
+            df,
+            index,
+            names_to,
+            names_pattern,
+            values_to,
+            sort_by_appearance,
+            ignore_index,
+        )
+
+    return _pivot_longer_names_pattern_sequence(
+        df, index, names_to, names_pattern, sort_by_appearance, ignore_index
+    )
+
+
+def _pivot_longer_frame_MultiIndex(
+    df: pd.DataFrame,
+    index,
+    sort_by_appearance: bool,
+    ignore_index: bool,
+    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.
+    """
+
+    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:
-        df = df.reset_index(level=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)
 
     if sort_by_appearance:
-        df = _sort_by_appearance_for_melt(
-            df=df, ignore_index=ignore_index, len_index=len_index
-        )
+        df = _sort_by_appearance_for_melt(df=df, len_index=len_index)
+
+    if ignore_index:
+        df.index = range(len(df))
+
+    return df
+
+
+def _pivot_longer_frame_single_Index(
+    df: pd.DataFrame,
+    index,
+    sort_by_appearance: bool,
+    ignore_index: bool,
+    values_to: str = None,
+) -> 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.
+    """
+
+    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
+            )
+            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 df.columns.names:
+        df = df.rename_axis(columns=None)
+
+    if sort_by_appearance:
+        df = _sort_by_appearance_for_melt(df=df, len_index=len_index)
+
+    if index:
+        df = df.reset_index(index)
 
-    elif ignore_index:
-        df.index = np.arange(len(df))
+    if ignore_index:
+        df.index = range(len(df))
 
     return df
 


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,5 +1,3 @@
-from itertools import product
-
 import numpy as np
 import pandas as pd
 import pytest
@@ -7,13 +5,16 @@
 
 from janitor import patterns
 
-df_checks = 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():
+    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
@@ -88,147 +89,71 @@ def df_multi():
     )
 
 
-index_labels = [2007, 20.09]
-column_labels = [2007, 20.99]
-names_to_labels = [1, {12, "newnames"}]
-
-index_does_not_exist = ["Region", ["207", "region"]]
-column_does_not_exist = ["two thousand and seven", ("2007", 2009)]
-
-index_type_checks = (
-    (frame, index) for frame, index in product([df_checks], index_labels)
-)
-column_type_checks = (
-    (frame, column_name)
-    for frame, column_name in product([df_checks], column_labels)
-)
-names_to_type_checks = (
-    (frame, names_to)
-    for frame, names_to in product([df_checks], names_to_labels)
-)
-
-names_to_sub_type_checks = (
-    (df_checks, (1, "rar")),
-    (df_checks, [{"set"}, 20]),
-)
-
-index_presence_checks = (
-    (frame, index)
-    for frame, index in product([df_checks], index_does_not_exist)
-)
-column_presence_checks = (
-    (frame, column_name)
-    for frame, column_name in product([df_checks], column_does_not_exist)
-)
-
-names_sep_not_required = [
-    (df_checks, "rar", "_"),
-    (df_checks, ["blessed"], ","),
-]
-
-names_sep_type_check = [
-    (df_checks, ["rar", "bar"], 1),
-    (df_checks, ("rar", "ragnar"), ["\\d+"]),
-]
-names_pattern_type_check = [
-    (df_checks, "rar", 1),
-    (df_checks, {1: "rar"}, {"\\d+"}),
-]
+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")
 
 
-@pytest.mark.parametrize("df,index", index_type_checks)
-def test_type_index(df, index):
+def test_type_index(df_checks):
     """Raise TypeError if wrong type is provided for the `index`."""
     with pytest.raises(TypeError):
-        df.pivot_longer(index=index)
+        df_checks.pivot_longer(index=2007)
 
 
-@pytest.mark.parametrize("df,column", column_type_checks)
-def test_type_column_names(df, column):
+def test_type_column_names(df_checks):
     """Raise TypeError if wrong type is provided for `column_names`."""
     with pytest.raises(TypeError):
-        df.pivot_longer(column_names=column)
+        df_checks.pivot_longer(column_names=2007)
 
 
-@pytest.mark.parametrize("df,names_to", names_to_type_checks)
-def test_type_names_to(df, names_to):
+def test_type_names_to(df_checks):
     """Raise TypeError if wrong type is provided for `names_to`."""
     with pytest.raises(TypeError):
-        df.pivot_longer(names_to=names_to)
+        df_checks.pivot_longer(names_to={2007})
 
 
-@pytest.mark.parametrize("df,names_to", names_to_sub_type_checks)
-def test_subtype_names_to(df, names_to):
+def test_subtype_names_to(df_checks):
     """
-    Raise TypeError if `names_to` is a list/tuple and the wrong type is
-    provided for entries in `names_to`.
+    Raise TypeError if `names_to` is a sequence
+    and the wrong type is provided for entries
+    in `names_to`.
     """
     with pytest.raises(TypeError):
-        df.pivot_longer(names_to=names_to)
-
+        df_checks.pivot_longer(names_to=[("year",)])
 
-@pytest.mark.parametrize("df,index", index_presence_checks)
-def test_presence_index(df, index):
-    """Raise KeyError if labels in `index` do not exist."""
-    with pytest.raises(KeyError):
-        df.pivot_longer(index=index)
 
-
-@pytest.mark.parametrize("df,column", column_presence_checks)
-def test_presence_columns(df, column):
-    """Raise KeyError if labels in `column_names` do not exist."""
-    with pytest.raises(KeyError):
-        df.pivot_longer(column_names=column)
+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=["_"])
 
 
-@pytest.mark.parametrize("df,names_to, names_sep", names_sep_not_required)
-def test_name_sep_names_to_len(df, names_to, names_sep):
-    """
-    Raise ValueError if `names_to` is a string,
-    or `names_to` is a list/tuple,
-    its length is one,
-    and `names_sep` is provided.
-    """
+def test_name_sep_no_names_to(test_df):
+    """Raise ValuError if `names_sep` and names_to is None."""
     with pytest.raises(ValueError):
-        df.pivot_longer(names_to=names_to, names_sep=names_sep)
-
+        test_df.pivot_longer(names_to=None, names_sep="_")
 
-@pytest.mark.parametrize("df,names_to, names_sep", names_sep_type_check)
-def test_name_sep_wrong_type(df, names_to, names_sep):
-    """Raise TypeError if the wrong type is provided for `names_sep`."""
-    with pytest.raises(TypeError):
-        df.pivot_longer(names_to=names_to, names_sep=names_sep)
 
-
-@pytest.mark.parametrize(
-    "df,names_to, names_pattern", names_pattern_type_check
-)
-def test_name_pattern_wrong_type(df, names_to, names_pattern):
+def test_name_pattern_wrong_type(test_df):
     """Raise TypeError if the wrong type provided for `names_pattern`."""
     with pytest.raises(TypeError):
-        df.pivot_longer(names_to=names_to, names_pattern=names_pattern)
+        test_df.pivot_longer(names_to=["set", ".value"], names_pattern=2007)
 
 
-def test_names_pattern_wrong_subtype():
-    """
-    Raise TypeError if `names_pattern` is a list/tuple
-    and wrong subtype is supplied.
-    """
-    with pytest.raises(TypeError):
-        df_checks.pivot_longer(
-            names_to=["variable", "value"], names_pattern=[1, "rar"]
-        )
+def test_name_pattern_no_names_to(test_df):
+    """Raise ValueError if `names_pattern` and names_to is None."""
+    with pytest.raises(ValueError):
+        test_df.pivot_longer(names_to=None, names_pattern="(.+)_(.+)")
 
 
-def test_names_pattern_names_to_wrong_type():
+def test_names_pattern_wrong_subtype(test_df):
     """
     Raise TypeError if `names_pattern` is a list/tuple
-    and wrong type is supplied for `names_to`.
+    and wrong subtype is supplied.
     """
     with pytest.raises(TypeError):
-        df_checks.pivot_longer(
-            names_to={"variable, value"}, names_pattern=["1", "rar"]
-        )
+        test_df.pivot_longer(names_to=["off", "pt"], names_pattern=[1, "pt"])
 
 
 def test_column_level_wrong_type(df_multi):
@@ -255,15 +180,15 @@ def test_ignore_index(test_df):
         )
 
 
-def test_duplicate_dot_value(test_df):
-    """Raise error if `names_to` contains more than one `.value."""
+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="(.+)_(.+)"
         )
 
 
-def test_names_pattern_names_to_unequal_length():
+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`.
@@ -274,7 +199,7 @@ def test_names_pattern_names_to_unequal_length():
         )
 
 
-def test_names_pattern_names_to_dot_value():
+def test_names_pattern_names_to_dot_value(df_checks):
     """
     Raise Error if `names_pattern` is a list/tuple and
     `.value` in `names_to`.
@@ -285,7 +210,7 @@ def test_names_pattern_names_to_dot_value():
         )
 
 
-def test_both_names_sep_and_pattern():
+def test_both_names_sep_and_pattern(df_checks):
     """
     Raise ValueError if both `names_sep`
     and `names_pattern` is provided.
@@ -322,13 +247,39 @@ def test_column_names_tuple_MultiIndex(df_multi):
         df_multi.pivot_longer(column_names=("names", "aa"))
 
 
-def test_values_to_wrong_type():
+def test_column_MultiIndex_names_sep(df_multi):
+    """
+    Raise ValueError if the dataframe's column is a MultiIndex,
+    and names_sep is present.
+    """
+    with pytest.raises(ValueError):
+        df_multi.pivot_longer(
+            column_names=[("names", "aa")],
+            names_sep="_",
+            names_to=["names", "others"],
+        )
+
+
+def test_column_MultiIndex_names_pattern(df_multi):
+    """
+    Raise ValueError if the dataframe's column is a MultiIndex,
+    and names_pattern is present.
+    """
+    with pytest.raises(ValueError):
+        df_multi.pivot_longer(
+            index=[("name", "a")],
+            names_pattern=r"(.+)(.+)",
+            names_to=["names", "others"],
+        )
+
+
+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():
+def test_values_to_exists_in_columns(df_checks):
     """
     Raise ValueError if `values_to` already
     exists in the dataframe's columns.
@@ -337,6 +288,14 @@ def test_values_to_exists_in_columns():
         df_checks.pivot_longer(values_to="region")
 
 
+def test_values_to_exists_in_names_to(df_checks):
+    """
+    Raise ValueError if `values_to` is in names_to.
+    """
+    with pytest.raises(ValueError):
+        df_checks.pivot_longer(values_to="year", names_to="year")
+
+
 def test_MultiIndex_column_level(df_multi):
     """Test output from MultiIndex column"""
     result = df_multi.pivot_longer(
@@ -351,18 +310,13 @@ def test_MultiIndex_column_level(df_multi):
 def test_pivot_no_args_passed():
     """Test output if no arguments are passed."""
     df_no_args = pd.DataFrame({"name": ["Wilbur", "Petunia", "Gregory"]})
-    df_no_args_output = pd.DataFrame(
-        {
-            "variable": ["name", "name", "name"],
-            "value": ["Wilbur", "Petunia", "Gregory"],
-        }
-    )
+
     result = df_no_args.pivot_longer()
 
-    assert_frame_equal(result, df_no_args_output)
+    assert_frame_equal(result, df_no_args.melt())
 
 
-def test_pivot_index_only_and_sort_by_appearance(df_checks_output):
+def test_pivot_index_only_and_sort_by_appearance(df_checks, df_checks_output):
     """
     Test output if only `index` is passed and
     `sort_by_apperance is ``True``.
@@ -376,7 +330,7 @@ def test_pivot_index_only_and_sort_by_appearance(df_checks_output):
     assert_frame_equal(result, df_checks_output)
 
 
-def test_pivot_index_only():
+def test_pivot_index_only(df_checks):
     """Test output if only `index` is passed."""
     result = df_checks.pivot_longer(
         index="region",
@@ -464,7 +418,7 @@ def test_ignore_index_false_same_length():
     assert_frame_equal(result, df_out)
 
 
-def test_pivot_column_only(df_checks_output):
+def test_pivot_column_only(df_checks, df_checks_output):
     """Test output if only `column_names` is passed."""
     result = df_checks.pivot_longer(
         column_names=["2007", "2009"],
@@ -475,7 +429,9 @@ def test_pivot_column_only(df_checks_output):
     assert_frame_equal(result, df_checks_output)
 
 
-def test_pivot_index_patterns_only_sort_by_appearance(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``.
@@ -504,7 +460,7 @@ def test_pivot_no_index_no_columns():
     assert_frame_equal(result, expected_output)
 
 
-def test_pivot_columns_patterns_only(df_checks_output):
+def test_pivot_columns_patterns_only(df_checks, df_checks_output):
     """Test output if the `patterns` function is passed to `column_names`."""
     result = df_checks.pivot_longer(
         column_names=patterns(r"\d+"),
@@ -515,13 +471,9 @@ def test_pivot_columns_patterns_only(df_checks_output):
     assert_frame_equal(result, df_checks_output)
 
 
-def test_length_mismatch():
-    """
-    Raise error if `names_to` is a list/tuple,
-    and its length  does not match
-    the number of extracted columns.
-    """
-    data = pd.DataFrame(
+@pytest.fixture
+def data():
+    return pd.DataFrame(
         {
             "country": ["United States", "Russia", "China"],
             "vault_2012_f": [48.132, 46.366, 44.266],
@@ -534,8 +486,88 @@ def test_length_mismatch():
             "floor_2016_m": [43.757, 44.766, 43.799],
         }
     )
+
+
+def test_length_mismatch(data):
+    """
+    Raise error if `names_to` is a list/tuple,
+    and its length  does not match
+    the number of extracted columns.
+    """
+
     with pytest.raises(ValueError):
-        data.pivot_longer(names_to=["event", "year"], names_sep="-")
+        data.pivot_longer(names_to=["event", "year"], names_sep="_")
+
+
+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",
+        )
+
+
+@pytest.fixture
+def start():
+    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],
+        }
+    )
+
+
+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_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)_.+",
+        )
+
+
+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_empty_mapping_all(test_df):
@@ -1160,7 +1192,6 @@ def test_multiple_values_sep(
                 "a": ["a", "b", "c"],
                 "A": ["A", "B", "C"],
             },
-            index=[0, 0, 0],
         ),
         "id",
         (".value", "instance"),
@@ -1887,6 +1918,7 @@ def test_single_value(
         None,
         "part",
         r"(.*)[_].*",
+        False,
     ),
     (
         pd.DataFrame(
@@ -1902,12 +1934,12 @@ def test_single_value(
         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": "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},
             ]
         ),
@@ -1915,16 +1947,26 @@ def test_single_value(
         patterns("_"),
         "part",
         r"(.*)[_].*",
+        True,
     ),
 ]
 
 
 @pytest.mark.parametrize(
-    "df_in,df_out,index, column_names,names_to,names_pattern",
+    """
+    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
+    df_in,
+    df_out,
+    index,
+    column_names,
+    names_to,
+    names_pattern,
+    sort_by_appearance,
 ):
     """
     Test output if `names_to` is a string and
@@ -1938,6 +1980,7 @@ def test_single_column_names_pattern(
         column_names=column_names,
         names_to=names_to,
         names_pattern=names_pattern,
+        sort_by_appearance=sort_by_appearance,
     )
 
     assert_frame_equal(result, df_out)
@@ -1978,7 +2021,6 @@ def test_group_present():
 
 # copied from :
 # https://github.com/pandas-dev/pandas/blob/master/pandas/tests/reshape/test_melt.py
-# interesting edge case
 def test_float_suffix_irregular():
     """
     Test output for floating suffixes for stubnames;
@@ -2238,3 +2280,62 @@ def test_multiple_column_names(df_in, df_out):
     )
 
     assert_frame_equal(result, df_out)
+
+
+def test_names_sep_non_unique_columns():
+    """Test output for non_unique columns with names_sep"""
+
+    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"},
+        ]
+    )
+
+    result = df.pivot_longer(
+        "event", names_to=(".value", "num"), names_sep="_", ignore_index=True
+    )
+    assert_frame_equal(result, expected)
+
+
+def test_names_pattern_sequence_single_unique_column():
+    """
+    Test output if names_pattern is a sequence,
+    and there is just one unique column.
+    """
+    df = pd.DataFrame(
+        {
+            "id": [1, 2, 3],
+            "x1": [4, 5, 6],
+            "x2": [5, 6, 7],
+        }
+    )
+
+    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},
+        ]
+    )
+
+    assert_frame_equal(result, expected)