Repository: pyjanitor-devs/pyjanitor

Issue:
Enhancements/Fixes/Suggestions for pivot_longer/pivot_wider
-  [X] use the `pivot` function instead of `stack/unstack`, for `pivot_wider`, since the minimum version is pandas 1.1 (the `pivot` function in versions prior to pandas 1.1 did not support lists in the `index`/`columns` parameters)
- ~~not sure if it is necessary to have an `aggfunc` parameter in `pivot_wider`. Maybe deprecate?~~
- ~~use `stack` where possible for `pivot_longer` - it should offer a simpler code and more maintainable~~
- ~~stick to single index columns for `pivot_longer` - more maintainable (current code looks spaghetti and brittle); any multiIndex column transformations can be done by the user with `stack` or `melt`. if accepted, this means deprecating the `col_level` parameter. also remove it from the `_select_columns` function.~~
- [X] Use the `ignore_index` capability from pandas `melt` directly, instead of the custom function created (since pandas minimum version in pyjanitor is now 1.1)
- [X] For `select_columns` return tuples as tuples (allows column names that are tuples to be selected)
- [X] remove `fill_value` from `complete`; user can always achieve this with better results with `fillna`
- [X] fix errors with groupby in `complete`


Discussion:

Resolving patch:
diff --git a/janitor/functions.py b/janitor/functions.py
--- a/janitor/functions.py
+++ b/janitor/functions.py
@@ -17,6 +17,7 @@
     NamedTuple,
     Optional,
     Pattern,
+    Sequence,
     Set,
     Tuple,
     Union,
@@ -3218,6 +3219,7 @@ def select_columns(
               .select_columns(['a', 'b', 'col_*'],
               invert=True))
 
+
     :param df: A pandas DataFrame.
     :param search_column_names: Valid inputs include:
 
@@ -3237,18 +3239,13 @@ def select_columns(
     .. # noqa: DAR402
     """
 
-    if isinstance(df.columns, pd.MultiIndex):
-        raise ValueError(
-            """
-            MultiIndex columns
-            not supported for `select_columns`.
-            """
-        )
-
     # applicable for any
-    # list-like object (ndarray, Series, pd.Index, tuple, ...)
-    if is_list_like(search_column_names):
-        search_column_names = list(search_column_names)
+    # list-like object (ndarray, Series, pd.Index, ...)
+    # excluding tuples, which are returned as is
+    if is_list_like(search_column_names) and (
+        not isinstance(search_column_names, tuple)
+    ):
+        search_column_names = [*search_column_names]
 
     full_column_list = _select_columns(search_column_names, df)
 
@@ -4442,26 +4439,22 @@ def expand_grid(
     df: Optional[pd.DataFrame] = None,
     df_key: Optional[str] = None,
     others: Optional[Dict] = None,
-    **kwargs,
 ) -> pd.DataFrame:
     """
     Creates a dataframe from a cartesian combination of all inputs.
 
-    This works with a dictionary of name value pairs,
-    or keyword arguments (`kwargs`);
-    it is also not restricted to dataframes;
+    This works with a dictionary of name value pairs.
+
+    It is also not restricted to dataframes;
     it can work with any list-like structure
     that is 1 or 2 dimensional.
-    MultiIndex objects are not supported though.
 
     If method-chaining to a dataframe,
     a key to represent the column name in the output must be provided.
 
-    Note that if a MultiIndex dataframe or series is passed, the index/columns
-    will be discarded, and a single indexed dataframe will be returned.
 
-    Existing data types are preserved in this function.
-    This includes Pandas' extension array dtypes.
+    Data types are preserved in this function,
+    including Pandas' extension array dtypes.
 
     The output will always be a dataframe.
 
@@ -4498,6 +4491,8 @@ def expand_grid(
         #  3 |   1
         #  3 |   2
 
+    .. note:: If a MultiIndex DataFrame or Series is passed, the index/columns
+        will be discarded, and a single indexed dataframe will be returned.
 
     Functional usage syntax:
 
@@ -4534,7 +4529,6 @@ def expand_grid(
         to be combined with the dataframe.
         If no dataframe exists, all inputs
         in others will be combined to create a dataframe.
-    :param kwargs: Keyword arguments are accepted.
     :returns: A pandas dataframe of all combinations of name value pairs.
     :raises TypeError: if `others` is not a dictionary
     :raises KeyError: if there is a dataframe and no key is provided.
@@ -4546,8 +4540,6 @@ def expand_grid(
 
     check("others", others, [dict])
 
-    others = {**others, **kwargs}
-
     # if there is a dataframe, for the method chaining,
     # it must have a key, to create a name value pair
     if df is not None:
@@ -4589,8 +4581,8 @@ def process_text(
     This modifies an existing column and can also be used to create a new
     column.
 
-    .. note:: In versions < 0.20.11, this function did not support creation of
-        new columns.
+    .. note:: In versions < 0.20.11, this function did not support the
+        creation of new columns.
 
     A list of all the string methods in Pandas can be accessed `here
     <https://pandas.pydata.org/docs/user_guide/text.html#method-summary>`__.
@@ -4741,7 +4733,7 @@ def process_text(
         # duplicated indices in the original dataframe
         df = df.set_index(np.arange(len(df)), append=True)  # extra_index_line
 
-    result = getattr(df.loc[:, column_name].str, string_function)(**kwargs)
+    result = getattr(df[column_name].str, string_function)(**kwargs)
 
     # TODO: Support for str.cat with `join` parameter
     # need a robust way to handle the results
@@ -5037,7 +5029,6 @@ def groupby_topk(
 def complete(
     df: pd.DataFrame,
     columns: List[Union[List, Tuple, Dict, str]] = None,
-    fill_value: Optional[Dict] = None,
     by: Optional[Union[list, str]] = None,
 ) -> pd.DataFrame:
     """
@@ -5117,12 +5108,9 @@ def complete(
         4  2004     Agarum         8.0
         5  2004     Saccharina     2.0
 
-    The null value can be replaced with the `fill_value` argument::
+    The null value can be replaced with the Pandas `fillna` argument::
 
-        df.complete(
-            columns = ['Year', 'Taxon'],
-            fill_value = {"Abundance" : 0}
-        )
+        df.complete(columns = ['Year', 'Taxon']).fillna(0)
 
            Year      Taxon     Abundance
         0  1999     Agarum         1.0
@@ -5136,26 +5124,23 @@ def complete(
     1999 to 2004? Easy - simply pass a dictionary pairing the column name
     with the new values::
 
-        df.complete(
-            columns = [{"Year": lambda df : range(df.Year.min(),
-                                                 df.Year.max() + 1)},
-                        "Taxon"],
-            fill_value={"Abundance" : 0}
-        )
+        new_year_values = lambda year: range(year.min(), year.max() + 1)
+
+        df.complete(columns = [{"Year": new_year_values}, "Taxon"])
 
-            Year      Taxon     Abundance
-        0   1999     Agarum         1.0
-        1   1999    Saccharina      4.0
-        2   2000     Agarum         0.0
-        3   2000    Saccharina      5.0
-        4   2001     Agarum         0.0
-        5   2001    Saccharina      0.0
-        6   2002     Agarum         0.0
-        7   2002    Saccharina      0.0
-        8   2003     Agarum         0.0
-        9   2003     Saccharina     0.0
-        10  2004     Agarum         8.0
-        11  2004    Saccharina      2.0
+            Year       Taxon  Abundance
+        0   1999      Agarum        1.0
+        1   1999  Saccharina        4.0
+        2   2000      Agarum        NaN
+        3   2000  Saccharina        5.0
+        4   2001      Agarum        NaN
+        5   2001  Saccharina        NaN
+        6   2002      Agarum        NaN
+        7   2002  Saccharina        NaN
+        8   2003      Agarum        NaN
+        9   2003  Saccharina        NaN
+        10  2004      Agarum        8.0
+        11  2004  Saccharina        2.0
 
     It is also possible to expose missing values within a groupby,
     by using the `by` parameter::
@@ -5172,8 +5157,7 @@ def complete(
     Let's get all the missing years per state::
 
         df.complete(
-            columns = [{'year': lambda df: np.arange(df.year.min(),
-                                                     df.year.max()+1)}],
+            columns = [{'year': new_year_values}],
             by='state'
         )
 
@@ -5213,7 +5197,6 @@ def complete(
                 (column1, column2, ...),
                 {column1: new_values, ...}
             ],
-            fill_value = None,
             by = label/list_of_labels
         )
 
@@ -5228,7 +5211,6 @@ def complete(
                 (column1, column2, ...),
                 {column1: new_values, ...},
             ],
-            fill_value=None,
             by = label/list_of_labels
         )
 
@@ -5237,13 +5219,10 @@ def complete(
         completed. It could be column labels (string type),
         a list/tuple of column labels, or a dictionary that pairs
         column labels with new values.
-    :param fill_value: Dictionary pairing the columns with the null
-        replacement value.
     :param by: label or list of labels to group by.
         The explicit missing values are returned per group.
     :returns: A pandas dataframe with modified column(s).
     :raises TypeError: if `columns` is not a list.
-    :raises TypeError: if `fill_value` is not a dictionary.
     :raises ValueError: if entry in `columns` is not a
         str/dict/list/tuple.
     :raises ValueError: if entry in `columns` is a dict/list/tuple
@@ -5257,7 +5236,7 @@ def complete(
 
     df = df.copy()
 
-    df = _computations_complete(df, columns, fill_value, by)
+    df = _computations_complete(df, columns, by)
 
     return df
 
@@ -5577,10 +5556,12 @@ def pivot_longer(
     `Team`. It then looks for columns that start with `Score` and collate all
     the values associated with these columns to a single column named `Score`.
 
-    You can also take advantage of `janitor.patterns` function, which allows
-    selection of columns via a regular expression; this can come in handy if
-    you have a lot of column names to pass to the `index` or `column_names`
-    paramenters, and you do not wish to manually type them all.
+    You can also take advantage of `janitor.patterns` function,
+    or the `select_columns` syntax,
+    which allows selection of columns via a regular expression;
+    this can come in handy if you have a lot of column names
+    to pass to the `index` or `column_names`  parameters,
+    and you do not wish to manually type them all.
 
     .. code-block:: python
 
@@ -5781,10 +5762,10 @@ 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,
-    names_sort: Optional[bool] = False,
+    index: Optional[Union[Sequence[str], str]] = None,
+    names_from: Optional[Union[Sequence[str], str]] = None,
+    values_from: Optional[Union[Sequence[str], str]] = None,
+    names_sort: Optional[bool] = True,
     flatten_levels: Optional[bool] = True,
     names_from_position: Optional[str] = "first",
     names_prefix: Optional[str] = None,
@@ -5797,7 +5778,7 @@ def pivot_wider(
     increased, while decreasing the number of rows.
 
     It is the inverse of the `pivot_longer` method, and is a
-    wrapper around `pd.DataFrame.unstack` method.
+    wrapper around `pd.DataFrame.pivot` method.
 
     This method does not mutate the original DataFrame.
 
@@ -5893,7 +5874,7 @@ def pivot_wider(
     .. note:: You may choose not to collapse the levels by passing `False`
         to the ``flatten_levels`` argument.
 
-    .. note:: An error is raised if the index is not unique and
+    .. note:: A ValueError is raised if the index is not unique and
         `aggfunc` is None.
 
     Functional usage syntax:
@@ -5959,24 +5940,23 @@ def pivot_wider(
         If ``values_from`` is not specified,
         all remaining columns will be used. If `flatten_levels` is ``False``,
         a MultiIndex dataframe is created.
-    :param names_sort: Default is `False`. Sorts columns by order of
-        appearance. Applicable only if ``flatten_levels`` is `True`.
+    :param names_sort: Default is `True`. Sorts columns by order of
+        appearance.
         Set as `True` to get the columns sorted lexicographicially,
         or if the columns are of category type.
     :param flatten_levels: Default is `True`. If `False`, the dataframe stays
         as a MultiIndex.
     :param names_from_position: By default, the values in ``names_from`` stay
-        at the front of the new column names, even when ``values_from`` or
-        ``aggfunc`` is a list. This can be changed to "last"; this places the
-        values in ``names_from`` at the tail of the column names. Applicable
-        only when ``flatten_levels`` is ``True``. Default is "first".
+        at the front of the new column names. This can be changed to "last";
+        this places the values in ``names_from``
+        at the tail of the column names.
     :param names_prefix: String to be added to the front of each output column.
         Can be handy if the values in ``names_from`` are numeric data types.
         Applicable only if ``flatten_levels`` is True.
     :param names_sep: If ``names_from`` or ``values_from`` contain multiple
         variables, this will be used to join their values into a single string
-        to use as a column name. Default is ``_``. Applicable only if
-        ``flatten_levels`` is ``True``.
+        to use as a column name. Default is ``_``.
+        Applicable only if ``flatten_levels`` is ``True``.
     :param aggfunc: An aggregate function. It can be a function, a string,
         list of functions, or a dictionary, pairing column name with aggregate
         function.
diff --git a/janitor/utils.py b/janitor/utils.py
--- a/janitor/utils.py
+++ b/janitor/utils.py
@@ -5,10 +5,11 @@
 import os
 import re
 import sys
+
 import warnings
 from collections import namedtuple
 from collections.abc import Callable as dispatch_callable
-from itertools import chain, product
+from itertools import chain, combinations
 from typing import (
     Callable,
     Dict,
@@ -28,8 +29,8 @@
     is_extension_array_dtype,
     is_list_like,
 )
-from pandas.core import common
 
+from pandas.core.common import apply_if_callable
 from .errors import JanitorError
 
 
@@ -436,37 +437,6 @@ def _wrapped(x, *args, **kwargs):
     return _wrapped
 
 
-def check_type_Index(value):
-    """
-    Returns True if dtype is pd.Index;.
-    returns False for MultiIndex.
-
-    # noqa: DAR201
-    # noqa: DAR101
-    """
-    if isinstance(value, pd.Index):
-        if isinstance(value, pd.MultiIndex):
-            return False
-        return True
-    return False
-
-
-def check_type_list(value):
-    """
-    Returns True if `value` is list-like.
-
-    Excludes pd.Series, pd.DataFrame,
-    np.ndarray, list, and pd.MultiIndex.
-
-    # noqa: DAR201
-    # noqa: DAR101
-    """
-    check1 = is_list_like(value)
-    check2 = (pd.DataFrame, pd.Series, np.ndarray, list, pd.MultiIndex)
-    check2 = not isinstance(value, check2)
-    return all((check1, check2))
-
-
 def _computations_expand_grid(others: dict) -> pd.DataFrame:
     """
     Creates a cartesian product of all the inputs in `others`.
@@ -480,7 +450,7 @@ def _computations_expand_grid(others: dict) -> pd.DataFrame:
 
     Another benefit of this approach,
     in addition to the significant performance gains,
-    is the preservation of data types. This is particularly noticeable for
+    is the preservation of data types. This is particularly relevant for
     Pandas' extension arrays dtypes (categoricals, nullable integers, ...).
 
     A dataframe of all possible combinations is returned.
@@ -489,42 +459,38 @@ def _computations_expand_grid(others: dict) -> pd.DataFrame:
     for key, _ in others.items():
         check("key", key, [str])
 
-    others = (
-        ([value], key) if is_scalar(value) else (value, key)
-        for key, value in others.items()
-    )
+    grid = {}
 
-    others = (
-        (pd.Series(value), key)
-        if is_extension_array_dtype(value) or check_type_Index(value)
-        else (value, key)
-        for value, key in others
-    )
-
-    others = (
-        (list(value), key) if check_type_list(value) else (value, key)
-        for value, key in others
-    )
+    for key, value in others.items():
+        if is_scalar(value):
+            grid[key] = pd.Series([value])
+        elif is_extension_array_dtype(value) and not (
+            isinstance(value, pd.Series)
+        ):
+            grid[key] = pd.Series(value)
+        elif is_list_like(value):
+            if not isinstance(
+                value, (pd.DataFrame, pd.Series, np.ndarray, list, pd.Index)
+            ):
+                grid[key] = list(value)
+            else:
+                grid[key] = value
 
-    others = [*others]  # list(others)
+    others = None
 
-    # this section gets the length of each data in others,
-    # creates a mesh, essentially a catersian product of indices
-    # for each data in `others`.
-    # the rest of the code then expands/explodes the data,
-    # based on its type, before concatenating into a dataframe.
-    mgrid_values = [slice(len(value)) for value, _ in others]
+    mgrid_values = [slice(len(value)) for _, value in grid.items()]
     mgrid_values = np.mgrid[mgrid_values]
     mgrid_values = map(np.ravel, mgrid_values)
-    others = zip(others, mgrid_values)
-    others = ((*left, right) for left, right in others)
-    others = (
+    grid = zip([*grid.items()], mgrid_values)
+    grid = ((*left, right) for left, right in grid)
+    grid = (
         _expand_grid(value, key, mgrid_values)
-        for value, key, mgrid_values in others
+        for key, value, mgrid_values in grid
     )
 
-    others = pd.concat(others, axis="columns", sort=False, copy=False)
-    return others
+    grid = pd.concat(grid, axis="columns", sort=False)
+
+    return grid
 
 
 @functools.singledispatch
@@ -538,8 +504,6 @@ def _expand_grid(value, key, mgrid_values, mode="expand_grid"):
     `_computations_complete` function.
     """
 
-    # this should exclude MultiIndex indexes,
-    # and any other non-supported data types.
     raise TypeError(
         f"{type(value).__name__} data type is not supported in `expand_grid`."
     )
@@ -549,15 +513,12 @@ def _expand_grid(value, key, mgrid_values, mode="expand_grid"):
 def _sub_expand_grid(value, key, mgrid_values):  # noqa: F811
     """
     Expands the list object based on `mgrid_values`.
-
     Converts to an array and passes it
     to the `_expand_grid` function for arrays.
-
     `mode` parameter is added, to make the function reusable
     in the `_computations_complete` function.
     Also, allowing `key` as None enables reuse in the
     `_computations_complete` function.
-
     Returns Series with name if 1-Dimensional array
     or DataFrame if 2-Dimensional array with column names.
     """
@@ -669,10 +630,47 @@ def _sub_expand_grid(  # noqa: F811
     return value
 
 
+@_expand_grid.register(pd.Index)
+def _sub_expand_grid(  # noqa: F811
+    value, key, mgrid_values, mode="expand_grid"
+):
+    """
+    Expands the Index based on `mgrid_values`.
+
+    `mode` parameter is added, to make the function reusable
+    in the `_computations_complete` function.
+    Also, allowing `key` as None enables reuse in the
+    `_computations_complete` function.
+
+    Checks for empty Index and returns modified keys.
+
+    Returns a DataFrame (if MultiIndex) with new column names,
+    or a Series with a new name.
+    """
+    if value.empty:
+        raise ValueError("""Index cannot be empty.""")
+
+    value = value.take(mgrid_values)
+
+    if mode != "expand_grid":
+        return value
+
+    if isinstance(value, pd.MultiIndex):
+        value = value.to_frame(index=False)
+        value.columns = value.columns.map(lambda column: f"{key}_{column}")
+    else:
+        value = value.to_series(index=np.arange(len(value)))
+        if value.name:
+            value.name = f"{key}_{value.name}"
+        else:
+            value.name = key
+
+    return value
+
+
 def _data_checks_complete(
     df: pd.DataFrame,
-    columns: List[Union[List, Tuple, Dict, str]] = None,
-    fill_value: Optional[Dict] = None,
+    columns: List[Union[List, Tuple, Dict, str]],
     by: Optional[Union[list, str]] = None,
 ):
     """
@@ -685,10 +683,10 @@ def _data_checks_complete(
     Also checks that the names in `columns` actually exist in `df`.
 
     Returns `df`, `columns`, `column_checker`,
-    `fill_value` and `by` if all checks pass.
+    and `by` if all checks pass.
 
     """
-    # TODO: get complete to work on MultiIndex columns,
+    # TODO: get `complete` to work on MultiIndex columns,
     # if there is sufficient interest with use cases
     if isinstance(df.columns, pd.MultiIndex):
         raise ValueError(
@@ -725,53 +723,67 @@ def _data_checks_complete(
     check_column(df, column_checker)
     column_checker_no_duplicates = None
 
-    if fill_value is not None:
-        check("fill_value", fill_value, [dict])
-        check_column(df, fill_value)
-
     if by is not None:
         if isinstance(by, str):
             by = [by]
         check("by", by, [list])
 
-    return df, columns, column_checker, fill_value, by
+    return df, columns, column_checker, by
 
 
 def _computations_complete(
     df: pd.DataFrame,
-    columns: List[Union[List, Tuple, Dict, str]] = None,
-    fill_value: Optional[Dict] = None,
+    columns: List[Union[List, Tuple, Dict, str]],
     by: Optional[Union[list, str]] = None,
 ) -> pd.DataFrame:
     """
     This function computes the final output for the `complete` function.
 
-    If `by` is present, then the index for df is set within this function;
-    else the index for `df` is set within `_base_complete`.
-    This allows setting the index once before computing, as against
-    setting the index multiple times, when running `apply` on the groupby,
-    with `_base_complete`.
+    If `by` is present, then groupby apply is used.
+
+    For some cases, the `stack/unstack` combination is preferred; it is more
+    efficient than `reindex`, as the size of the data grows. It is only
+    applicable if all the entries in `columns` are strings, there are
+    no nulls(stacking implicitly removes nulls in columns),
+    the length of `columns` is greater than 1, and the index
+    has no duplicates.
+
+    If there is a dictionary in `columns`, it is possible that all the values
+    of a key, or keys, may not be in the existing column with the same key(s);
+    as such, a union of the current index and the generated index is executed,
+    to ensure that all combinations are in the final dataframe.
 
     A dataframe, with rows of missing values, if any, is returned.
     """
 
-    df, columns, column_checker, fill_value, by = _data_checks_complete(
-        df, columns, fill_value, by
+    df, columns, column_checker, by = _data_checks_complete(df, columns, by)
+
+    dict_present = any((isinstance(entry, dict) for entry in columns))
+    all_strings = all(isinstance(column, str) for column in columns)
+
+    df = df.set_index(column_checker)
+
+    df_index = df.index
+    df_names = df_index.names
+
+    any_nulls = any(
+        df_index.get_level_values(name).hasnans for name in df_names
     )
+
     if not by:
-        df = _base_complete(df, column_checker, columns)
 
+        df = _base_complete(df, columns, all_strings, any_nulls, dict_present)
+
+    # a better (and faster) way would be to create a dataframe
+    # from the groupby ...
+    # solution here got me thinking
+    # https://stackoverflow.com/a/66667034/7175713
+    # still thinking on how to improve speed of groupby apply
     else:
-        df = df.set_index(column_checker, drop=False)
-        if not df.index.is_monotonic_increasing:
-            df = df.sort_index()
         df = df.groupby(by).apply(
-            _base_complete, column_checker, columns, True
+            _base_complete, columns, all_strings, any_nulls, dict_present,
         )
-        df = df.drop(columns=by + column_checker)
-
-    if fill_value:
-        df = df.fillna(fill_value)
+        df = df.drop(columns=by)
 
     df = df.reset_index()
 
@@ -780,81 +792,113 @@ def _computations_complete(
 
 def _base_complete(
     df: pd.DataFrame,
-    column_checker: List,
-    columns: List[Union[List, Dict, str]],
-    by: bool = False,
+    columns: List[Union[List, Tuple, Dict, str]],
+    all_strings: bool,
+    any_nulls: bool,
+    dict_present: bool,
 ) -> pd.DataFrame:
-    """
-    This is the main workhorse of the `complete` function.
 
-    It will be reused in `computation_complete`.
+    df_empty = df.empty
+    df_index = df.index
+    unique_index = df_index.is_unique
+    columns_to_stack = None
+
+    if all_strings and (not any_nulls) and (len(columns) > 1) and unique_index:
+        if df_empty:
+            df["dummy"] = 1
+
+        columns_to_stack = columns[1:]
+        df = df.unstack(columns_to_stack)  # noqa: PD010
+        df = df.stack(columns_to_stack, dropna=False)  # noqa: PD013
+        if df_empty:
+            df = df.drop(columns="dummy")
+        columns_to_stack = None
+        return df
+
+    indexer = _create_indexer_for_complete(df_index, columns)
+
+    if unique_index:
+        if dict_present:
+            indexer = df_index.union(indexer, sort=None)
+        df = df.reindex(indexer)
+
+    else:
+        df = df.join(pd.DataFrame([], index=indexer), how="outer")
+
+    return df
+
 
-    A Dataframe, with rows of missing values, if any, is returned.
+def _create_indexer_for_complete(
+    df_index: pd.Index, columns: List[Union[List, Dict, str]],
+) -> pd.DataFrame:
     """
+    This creates the index that will be used
+    to expand the dataframe in the `complete` function.
 
-    dict_present = any((isinstance(entry, dict) for entry in columns))
+    A pandas Index is returned.
+    """
 
-    indexer = [_complete_column(column, df) for column in columns]
+    complete_columns = (
+        _complete_column(column, df_index) for column in columns
+    )
 
-    if dict_present:
-        indexer = (
-            [entry] if not isinstance(entry, list) else entry
-            for entry in indexer
-        )
-        indexer = [*chain.from_iterable(indexer)]
+    complete_columns = (
+        (entry,) if not isinstance(entry, list) else entry
+        for entry in complete_columns
+    )
+    complete_columns = chain.from_iterable(complete_columns)
+    indexer = [*complete_columns]
 
     if len(indexer) > 1:
+        indexer = _complete_indexer_expand_grid(indexer)
 
-        mgrid_values = [slice(len(value)) for value in indexer]
-        mgrid_values = np.mgrid[mgrid_values]
-        mgrid_values = map(np.ravel, mgrid_values)
-
-        indexer = zip(indexer, mgrid_values)
-        indexer = [
-            _expand_grid(value, None, mgrid_values, mode=None)
-            for value, mgrid_values in indexer
-        ]
-        indexer = pd.concat(indexer, axis="columns")
-        if not by:
-            df = df.set_index(column_checker)
-            if not df.index.is_monotonic_increasing:
-                df = df.sort_index()
-            indexer = indexer.set_index(column_checker)
     else:
         indexer = indexer[0]
-        indexer = pd.DataFrame([], index=indexer)
 
-    if not indexer.index.is_monotonic_increasing:
-        indexer = indexer.sort_index()
+    return indexer
 
-    if df.index.is_unique:
-        # dictionary is not bound to contain same values
-        # as in the dataframe's column being referred to.
-        # As such, this checks if all values can be found
-        # in the new index; if True, then reindex is safe
-        # if not, then the join ensures no data is lost.
-        if dict_present:
-            if df.index.isin(indexer.index).all():
-                df = df.reindex(indexer.index)
-            else:
-                df = df.join(indexer, how="outer", sort=False)
-        else:
-            df = df.reindex(indexer.index)
 
-    else:
-        df = df.join(indexer, how="outer", sort=False)
+def _complete_indexer_expand_grid(indexer):
+    """
+    Generate indices to expose explicitly missing values,
+    using the `expand_grid` function.
 
-    return df
+    Returns a pandas Index.
+    """
+    indexers = []
+    mgrid_values = [slice(len(value)) for value in indexer]
+    mgrid_values = np.mgrid[mgrid_values]
+    mgrid_values = map(np.ravel, mgrid_values)
+
+    indexer = zip(indexer, mgrid_values)
+    indexer = (
+        _expand_grid(value, None, mgrid_values, mode=None)
+        for value, mgrid_values in indexer
+    )
+
+    for entry in indexer:
+        if isinstance(entry, pd.MultiIndex):
+            names = entry.names
+            val = (entry.get_level_values(name) for name in names)
+            indexers.extend(val)
+        else:
+            indexers.append(entry)
+    indexer = pd.MultiIndex.from_arrays(indexers)
+    indexers = None
+    return indexer
 
 
 @functools.singledispatch
-def _complete_column(column, df):
+def _complete_column(column, index):
     """
-    This function processes the labels/entries in the
-    `columns` argument, to ultimately create an Index,
-    possibly from a cartesian product,
-    to reindex the original dataframe and expose the
-    possibly missing values.
+    This function processes the `columns` argument,
+    to create a pandas Index or a list.
+
+    Args:
+        column : str/list/dict
+        index: pandas Index
+
+    A unique pandas Index or a list of unique pandas Indices is returned.
     """
     raise TypeError(
         """This type is not supported in the `complete` function."""
@@ -862,70 +906,114 @@ def _complete_column(column, df):
 
 
 @_complete_column.register(str)  # noqa: F811
-def _sub_complete_column(column, df):  # noqa: F811
+def _sub_complete_column(column, index):  # noqa: F811
     """
-    This function processes the labels/entries in the
-    `columns` argument, to ultimately create an Index,
-    possibly from a cartesian product,
-    to reindex the original dataframe and expose the
-    possibly missing values.
+    This function processes the `columns` argument,
+    to create a pandas Index.
 
-    A Series is returned.
+    Args:
+        column : str
+        index: pandas Index
+
+    Returns:
+        pd.Index: A pandas Index with a single level
     """
 
-    arr = df[column]
+    arr = index.get_level_values(column)
+
     if not arr.is_unique:
         arr = arr.drop_duplicates()
     return arr
 
 
 @_complete_column.register(list)  # noqa: F811
-def _sub_complete_column(column, df):  # noqa: F811
+def _sub_complete_column(column, index):  # noqa: F811
     """
-    This function processes the labels/entries in the
-    `columns` argument, to ultimately create an Index,
-    possibly from a cartesian product,
-    to reindex the original dataframe and expose the
-    possibly missing values.
+    This function processes the `columns` argument,
+    to create a pandas Index.
+
+    Args:
+        column : list
+        index: pandas Index
 
-    A DataFrame is returned.
+    Returns:
+        pd.MultiIndex
     """
 
-    arr = df.loc[:, column]
-    if arr.duplicated().any(axis=None):
-        arr = arr.drop_duplicates()
+    level_to_drop = [name for name in index.names if name not in column]
+    arr = index.droplevel(level_to_drop)
+    if not arr.is_unique:
+        return arr.drop_duplicates()
     return arr
 
 
 @_complete_column.register(dict)  # noqa: F811
-def _sub_complete_column(column, df):  # noqa: F811
+def _sub_complete_column(column, index):  # noqa: F811
     """
-    This function processes the labels/entries in the
-    `columns` argument, to ultimately create an Index,
-    possibly from a cartesian product,
-    to reindex the original dataframe and expose the
-    possibly missing values.
+    This function processes the `columns` argument,
+    to create a pandas Index or a list.
 
-    A list of Series is returned.
+    Args:
+        column : dict
+        index: pandas Index
+
+    Returns:
+        list: A list of unique pandas Indices.
     """
 
     collection = []
     for key, value in column.items():
-        arr = common.apply_if_callable(value, df)
-        if not isinstance(arr, pd.Series):
-            try:
-                arr = pd.Series(arr)
-            except ValueError:
-                raise ValueError(
-                    """
-                    It seems the supplied pair in the dictionary
-                    cannot be converted to a 1-dimensional object.
-                    """
-                )
+        arr = apply_if_callable(value, index.get_level_values(key))
+        if not is_list_like(arr):
+            raise ValueError(
+                """
+                Input in the supplied dictionary
+                must be list-like.
+                """
+            )
+        if (
+            not isinstance(
+                arr, (pd.DataFrame, pd.Series, np.ndarray, pd.Index)
+            )
+        ) and (not is_extension_array_dtype(arr)):
+            arr = pd.Index([*arr], name=key)
+
+        if arr.ndim != 1:
+            raise ValueError(
+                """
+                It seems the supplied pair in the supplied dictionary
+                cannot be converted to a 1-dimensional Pandas object.
+                Kindly provide data that can be converted to
+                a 1-dimensional Pandas object.
+                """
+            )
+        if isinstance(arr, pd.MultiIndex):
+            raise ValueError(
+                """
+                MultiIndex object not acceptable
+                in the supplied dictionary.
+                """
+            )
+
+        if not isinstance(arr, pd.Index):
+            arr = pd.Index(arr, name=key)
+
+        if arr.empty:
+            raise ValueError(
+                """
+                Input in the supplied dictionary
+                cannot be empty.
+                """
+            )
+
         if not arr.is_unique:
             arr = arr.drop_duplicates()
-        arr.name = key
+
+        if arr.name is None:
+            arr.name = key
+
         collection.append(arr)
+
     return collection
 
 
@@ -957,27 +1045,29 @@ def _data_checks_pivot_longer(
 
     if column_level is not None:
         check("column_level", column_level, [int, str])
+        df.columns = df.columns.get_level_values(column_level)
 
     if index is not None:
-        if is_list_like(index) and not isinstance(index, tuple):
+        if is_list_like(index) and (not isinstance(index, tuple)):
             index = list(index)
-        index = _select_columns(index, df, level=column_level)
+        index = _select_columns(index, df)
 
     if column_names is not None:
-        if is_list_like(column_names) and not isinstance(column_names, tuple):
+        if is_list_like(column_names) and (
+            not isinstance(column_names, tuple)
+        ):
             column_names = list(column_names)
-        column_names = _select_columns(column_names, df, level=column_level)
+        column_names = _select_columns(column_names, df)
 
     if isinstance(names_to, str):
         names_to = [names_to]
 
-    else:
-        if isinstance(names_to, tuple):
-            names_to = list(names_to)
+    elif isinstance(names_to, tuple):
+        names_to = list(names_to)
 
     check("names_to", names_to, [list])
 
-    if not all(isinstance(word, str) for word in 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 len(names_to) > 1:
@@ -989,11 +1079,12 @@ def _data_checks_pivot_longer(
                 """
             )
 
-        if ".value" in names_to and names_to.count(".value") > 1:
+        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:
+    if (len(names_to) == 1) and (names_sep is not None):
         raise ValueError(
             """
             For a single `names_to` value,
@@ -1009,7 +1100,7 @@ def _data_checks_pivot_longer(
             ):
                 raise TypeError(
                     """
-                    All entries in ``names_pattern`` argument
+                    All entries in the ``names_pattern`` argument
                     must be regular expressions.
                     """
                 )
@@ -1035,7 +1126,7 @@ def _data_checks_pivot_longer(
 
     check("values_to", values_to, [str])
 
-    if values_to in df.columns and any(
+    if (values_to in df.columns) and any(
         (
             ".value" not in names_to,
             not isinstance(names_pattern, (list, tuple)),
@@ -1051,41 +1142,41 @@ def _data_checks_pivot_longer(
             """
         )
 
-    if any((names_sep, names_pattern)) and isinstance(
-        df.columns, pd.MultiIndex
+    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)):
         # adapted from pandas' melt source code
         if (
-            index is not None
+            (index is not None)
             and isinstance(df.columns, pd.MultiIndex)
-            and not isinstance(index, list)
+            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 columns are a MultiIndex.
+                """
             )
 
         if (
-            column_names is not None
+            (column_names is not None)
             and isinstance(df.columns, pd.MultiIndex)
-            and not isinstance(column_names, list)
+            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 columns are a MultiIndex.
+                """
             )
 
     check("sort_by_appearance", sort_by_appearance, [bool])
@@ -1106,49 +1197,14 @@ def _data_checks_pivot_longer(
     )
 
 
-def __tile_compat(arr: pd.Index, df: pd.DataFrame) -> pd.Index:
-    """
-    Repeats index multiple times.
-    """
-    # adapted from pandas.core.reshape.utils (_tile_compat)
-    # applies to MultiIndexes as well
-    # numpy resize, with len(df) as length works too
-    taker = np.tile(np.arange(len(arr)), len(df) // len(arr))
-    return arr.take(taker)
-
-
-def _restore_index_and_sort_by_appearance(
-    df: pd.DataFrame, ignore_index, sort_by_appearance, df_index
+def _sort_by_appearance_for_melt(
+    df: pd.DataFrame, ignore_index: bool, len_index: int
 ) -> pd.DataFrame:
     """
-    This function restores the original index via the `ignore_index`
-    and `df_index` parameters, and sorts the resulting dataframe
-    by appearance, via the `sort_by_appearance` parameter.
-    It is meant for sections in the `_computations_pivot_longer`
-    function that do not have `.value` in the dataframe's column names.
-
-    An example for `ignore_index`:
-
-    Say we have data like below:
-           A  B  C
-        0  a  1  2
-        1  b  3  4
-        2  c  5  6
-
-    If `ignore_index` is False (this means the original index will be reused),
-    then the resulting dataframe will look like below:
+    This function sorts the resulting dataframe by appearance,
+    via the `sort_by_appearance` parameter in `computations_pivot_longer`.
 
-           A  variable  value
-        0  a        B      1
-        1  b        B      3
-        2  c        B      5
-        0  a        C      2
-        1  b        C      4
-        2  c        C      6
-
-    Note how the index is repeated ([0,1,2,0,1,2]).
-
-    An example for `sort_by_appearance` :
+    An example for `sort_by_appearance`:
 
     Say data looks like this :
         id, a1, a2, a3, A1, A2, A3
@@ -1163,38 +1219,24 @@ def _restore_index_and_sort_by_appearance(
     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 possibly reindexed or sorted by appearance
-    is returned.
+    A dataframe that is sorted by appearance is returned.
     """
-    primary_index_sorter = None
+
     index_sorter = None
 
-    if sort_by_appearance:
-        # index is already an array of integers(from the melt operation)
-        # so let's use it
-        primary_index_sorter = df.index
-
-    # if the height of the new dataframe is the same as the old
-    # dataframe, simply reuse the original index.
-    if not ignore_index:
-        if not len(df_index) == len(df):
-            df.index = __tile_compat(df_index, df)
-        else:
-            df.index = df_index
+    # 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)))
 
-    length_check = any((len(df_index) == 1, len(df_index) == len(df)))
-    if sort_by_appearance:
-        # 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
-        if not length_check:
-            index_sorter = np.reshape(
-                primary_index_sorter, (-1, len(df_index))
-            ).ravel(order="F")
-            df = df.take(index_sorter)
+    if not length_check:
+        index_sorter = np.reshape(np.arange(len(df)), (-1, len_index)).ravel(
+            order="F"
+        )
+        df = df.take(index_sorter)
 
         if ignore_index:
-            df.index = primary_index_sorter
+            df.index = np.arange(len(df))
 
     return df
 
@@ -1203,7 +1245,7 @@ def _pivot_longer_extractions(
     df: pd.DataFrame,
     index: Optional[Union[List, Tuple]] = None,
     column_names: Optional[Union[List, Tuple]] = None,
-    names_to: Optional[Union[List, Tuple, str]] = None,
+    names_to: Optional[List] = None,
     names_sep: Optional[Union[str, Pattern]] = None,
     names_pattern: Optional[
         Union[
@@ -1217,166 +1259,163 @@ def _pivot_longer_extractions(
     into new columns, and is executed if `names_sep` or `names_pattern`
     is not None.
 
-    A tuple, containing the dataframe and `others`, are returned.
+    A dataframe is returned.
     """
 
-    # This is executed if the user is only interested in a subsection
-    # put here because it is specific only for situations where `names_sep`
-    # or `names_pattern` is not None
-    if all((index, column_names)):
-        df = df.filter(index + column_names)
-
-    dot_value = any(
-        ((".value" in names_to), isinstance(names_pattern, (list, tuple)))
-    )
-
-    if dot_value:
-        # the extra indices (np.arange(len(df))) serve as a way to
-        # associate each unpivoted value to a unique row identifier.
-        # It also aids in sorting by appearance.
-        # append is used in set_index,
-        # in case the user wishes to keep the original index.
+    if any((names_sep, names_pattern)):
         if index:
-            df = df.set_index([*index, np.arange(len(df))], append=True)
-        else:
-            df = df.set_index(np.arange(len(df)), append=True)
+            df = df.set_index(index, append=True)
+
+        if column_names:
+            df = df.loc[:, column_names]
 
     mapping = None
     if names_sep:
-        mapping = pd.Series(df.columns).str.split(names_sep, expand=True)
+        mapping = df.columns.str.split(names_sep, expand=True)
 
-        if len(mapping.columns) != len(names_to):
+        if len(mapping.names) != len(names_to):
             raise ValueError(
                 """
                 The length of ``names_to`` does not match
                 the number of columns extracted.
                 """
             )
-        mapping.columns = names_to
+        mapping.names = names_to
 
-    else:
-        if isinstance(names_pattern, str):
-            mapping = df.columns.str.extract(names_pattern)
+    elif isinstance(names_pattern, str):
+        mapping = df.columns.str.extract(names_pattern, expand=True)
 
-            if mapping.isna().all(axis=None):
-                raise ValueError(
-                    """
-                    The regular expression in ``names_pattern``
-                    did not return any matches.
-                    """
-                )
-            if len(names_to) != len(mapping.columns):
-                raise ValueError(
-                    """
-                    The length of ``names_to`` does not match
-                    the number of columns extracted.
-                    """
-                )
-            mapping.columns = names_to
-
-        else:  # list/tuple of regular expressions
-            mapping = [
-                df.columns.str.contains(regex) for regex in names_pattern
-            ]
-
-            if not np.any(mapping):
-                raise ValueError(
-                    """
-                    The regular expressions in ``names_pattern``
-                    did not return any matches.
-                    """
-                )
-            mapping = np.select(mapping, names_to, None)
-            mapping = pd.DataFrame(mapping, columns=[".value"])
+        if mapping.isna().all(axis=None):
+            raise ValueError(
+                """
+                The regular expression in ``names_pattern``
+                did not return any matches.
+                """
+            )
 
-    group = None
-    others = None
-    positions = None
-    category_dtypes = None
-    category_keys = None
-    reindex_columns = None
-    if not dot_value and index:
-        # more efficient to do this, than having to
-        # set the index and resetting.
-        # This puts the index(or indices) into the first
-        # column and set the remaining columns to empty string
-        # on the same row(s). When melting, pd.melt will
-        # gracefully handle the empty cells and replicate the
-        # `index` values accordingly.
-        positions = df.columns.get_indexer(index)
-        mapping.iloc[positions, 0] = index
-        mapping.iloc[positions, 1:] = ""
+        if mapping.isna().any(axis=None):
+            raise ValueError(
+                """
+                The regular expression in ``names_pattern``
+                did not return all matches.
+                Kindly provide a regular expression that
+                captures all patterns.
+                """
+            )
 
-    else:
-        # this gets the complete pairings of all labels in `mapping`
-        reindex_columns = [column.unique() for _, column in mapping.items()]
-        actual_values = [column for _, column in mapping.items()]
-        actual_values = zip(*actual_values)
-        # check if `mapping` is already 'complete'
-        if set(product(*reindex_columns)).difference(actual_values):
-            reindex_columns = product(*reindex_columns)
-            reindex_columns = pd.DataFrame(
-                reindex_columns, columns=mapping.columns
+        if len(names_to) != len(mapping.columns):
+            raise ValueError(
+                """
+                The length of ``names_to`` does not match
+                the number of columns extracted.
+                """
             )
+
+        if len(mapping.columns) == 1:
+            mapping = pd.Index(mapping.iloc[:, 0], name=names_to[0])
         else:
-            reindex_columns = None
+            mapping = pd.MultiIndex.from_frame(mapping, names=names_to)
 
-        others = [
-            column_name for column_name in mapping if column_name != ".value"
+    elif isinstance(names_pattern, (list, tuple)):
+        mapping = [
+            df.columns.str.contains(regex, na=False) for regex in names_pattern
         ]
 
-        # creating categoricals allows us to sort the data; this is necessary
-        # because we will be splitting the data into different chunks and
-        # concatenating back into one whole. As such, having the data uniform
-        # in all the chunks before merging back is crucial. Also, having
-        # categoricals ensure the data stays in its original form in terms
-        # of first appearance.
-        if others:
-            category_dtypes = {
-                key: CategoricalDtype(
-                    categories=column.dropna().unique(), ordered=True
-                )
-                if column.hasnans
-                else CategoricalDtype(categories=column.unique(), ordered=True)
-                for key, column in mapping.loc[:, others].items()
-            }
-            mapping = mapping.astype(category_dtypes)
-        # creating a group is primarily for sorting by appearance.
-        # The secondary purpose is to indicate groups if a list/tuple
-        # is passed to `names_pattern`.
-        group = "group"
-        for column_name in mapping:
-            if column_name == group:
-                group = f"{group}_1"
-        mapping.loc[:, group] = mapping.groupby(".value").cumcount()
-        others.append(group)
-
-        if reindex_columns is not None:
-            reindex_columns = mapping.merge(
-                reindex_columns, how="outer"
-            ).fillna(  # fill the null values in `group`
-                0
+        if not np.any(mapping):
+            raise ValueError(
+                """
+                The regular expressions in ``names_pattern``
+                did not return any matches.
+                """
             )
-            reindex_columns = pd.MultiIndex.from_frame(reindex_columns)
 
-    if len(mapping.columns) > 1:
-        df.columns = pd.MultiIndex.from_frame(mapping)
-    else:
-        df.columns = mapping.iloc[:, 0]
+        mapping = np.select(mapping, names_to, None)
+        mapping = pd.Index(mapping, name=".value")
 
+        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.
+                """
+            )
+
+    df.columns = mapping
+
+    dot_value = any(
+        ((".value" in names_to), isinstance(names_pattern, (list, tuple)))
+    )
+
+    first = None
+    last = None
+    complete_index = None
+    dtypes = None
+    cumcount = None
     if dot_value:
-        if reindex_columns is not None:
-            df = df.reindex(columns=reindex_columns)
-            # this is used in `computations_pivot_longer`
-            # to retain only the values or pairings in the
-            # original dataframe's columns
-            if category_dtypes:
-                category_keys = list(category_dtypes.keys())
-                mapping = mapping.set_index(category_keys).index
+        if not isinstance(mapping, pd.MultiIndex):
+            dtypes = CategoricalDtype(
+                categories=mapping.drop_duplicates(), ordered=True
+            )
+            df.columns = df.columns.astype(dtypes)
+
         else:
-            mapping = None
+            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.drop_duplicates(), ordered=True
+                )
+                for entry in mapping
+            ]
+            mapping = [
+                entry.astype(dtype) for entry, dtype in zip(mapping, dtypes)
+            ]
+
+            if cumcount_check:
+                mapping.append(cumcount)
+            mapping = pd.MultiIndex.from_arrays(mapping)
+            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 df, others, group, mapping
+            # 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]
+                    )
+
+                else:
+                    complete_index = pd.MultiIndex.from_product(mapping.levels)
+
+                df = df.reindex(columns=complete_index)
+            if cumcount_check:
+                df = df.droplevel(-1, axis="columns")
+
+    return df
 
 
 def _computations_pivot_longer(
@@ -1434,7 +1473,7 @@ def _computations_pivot_longer(
     if (
         (index is None)
         and column_names
-        and len(df.columns) > len(column_names)
+        and (len(df.columns) > len(column_names))
     ):
         index = [
             column_name
@@ -1442,7 +1481,7 @@ def _computations_pivot_longer(
             if column_name not in column_names
         ]
 
-    df_index = df.index
+    len_index = len(df)
 
     # scenario 1
     if all((names_pattern is None, names_sep is None)):
@@ -1454,91 +1493,83 @@ def _computations_pivot_longer(
             var_name=names_to,
             value_name=values_to,
             col_level=column_level,
-        )
-
-        df = _restore_index_and_sort_by_appearance(
-            df=df,
             ignore_index=ignore_index,
-            sort_by_appearance=sort_by_appearance,
-            df_index=df_index,
         )
 
+        if sort_by_appearance:
+            df = _sort_by_appearance_for_melt(
+                df=df, ignore_index=ignore_index, len_index=len_index
+            )
+
         return df
 
-    if any((names_pattern, names_sep)):
-        df, others, group, mapping = _pivot_longer_extractions(
-            df=df,
-            index=index,
-            column_names=column_names,
-            names_to=names_to,
-            names_sep=names_sep,
-            names_pattern=names_pattern,
+    df = _pivot_longer_extractions(
+        df=df,
+        index=index,
+        column_names=column_names,
+        names_to=names_to,
+        names_sep=names_sep,
+        names_pattern=names_pattern,
+    )
+
+    df_columns = df.columns
+
+    if ".value" not in df_columns.names:
+        df = pd.melt(
+            df, id_vars=None, value_name=values_to, ignore_index=False
         )
 
-        if ".value" not in df.columns.names:
-            df = pd.melt(df, id_vars=index, value_name=values_to)
+    else:  # .value
+        if df_columns.nlevels == 1:
+            df = [
+                df[name].melt(ignore_index=False)
+                # changing the name in `rename`
+                # instead of passing it to `melt`
+                # avoids name collision,
+                # especially if `name` exists in `df`
+                # during melt
+                .iloc[:, -1].rename(name)
+                for name in df_columns.unique()
+            ]
+            df = pd.concat(df, axis=1)
 
-            df = _restore_index_and_sort_by_appearance(
-                df=df,
-                ignore_index=ignore_index,
-                sort_by_appearance=sort_by_appearance,
-                df_index=df_index,
-            )
-            return df
-
-        # .value
-        stubnames = df.columns.get_level_values(".value").unique()
-        df_index = df.index
-        # ensures data is uniform in all the chunks
-        df = df.sort_index(axis="columns", level=others, sort_remaining=False)
-        # separate into chunks by stubname
-        if len(stubnames) > 1:
+        else:
+            # 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=stub, level=".value", axis="columns").melt(
-                    value_name=stub
+                df.xs(key=name, level=".value", axis=1).melt(
+                    ignore_index=False, value_name=name
                 )
-                for stub in stubnames
+                for name in df_columns.get_level_values(".value").unique()
             ]
 
             first, *rest = df
-            # the `first` frame already has `others`,
-            # `others` can be safely discarded from the other dataframes
-            # this is why sorting was done earlier (via the sort_index)
-            # to ensure uniformity
-            rest = [frame.iloc[:, -1] for frame in rest]
-            df = pd.concat([first, *rest], axis="columns")
-
-        else:
-            df = df.xs(key=stubnames[0], level=".value", axis="columns").melt(
-                value_name=stubnames[0]
-            )
-
-        intersection = None
-        if mapping is not None:
-            # if the values or the pairings of the values
-            # do not exist in the original dataframe's columns,
-            # then discard.
-            # a tiny bit faster than merge
-            intersection = df.set_index(mapping.names).index.isin(mapping)
-            df = df.loc[intersection]
 
-        # attach df_index
-        df.index = __tile_compat(df_index, df)
+            # `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 sort_by_appearance:
-            df = df.set_index(group, append=True)
-            # sorting by the extra index(-2) and group(-1)
-            df = df.sort_index(level=[-2, -1], sort_remaining=False)
-            df = df.droplevel(level=[-2, -1])
-        else:
-            df = df.drop(columns=group)
-            df = df.droplevel(level=-1)  # the extra index
+    if index:
+        df = df.reset_index(level=index)
 
-        if index:
-            df = df.reset_index(level=index)
+    if sort_by_appearance:
+        df = _sort_by_appearance_for_melt(
+            df=df, ignore_index=ignore_index, len_index=len_index
+        )
 
-        if ignore_index:
-            df.index = np.arange(len(df))
+    elif ignore_index:
+        df.index = np.arange(len(df))
 
     return df
 
@@ -1640,38 +1671,16 @@ def _computations_pivot_wider(
     """
     This is the main workhorse of the `pivot_wider` function.
 
-    The `unstack` method is used here, and not `pivot`; multiple
-    labels in the `index` or `names_from` are supported in the
-    `pivot` function for Pandas 1.1 and above. Besides, pandas'
-    `pivot` function is built on the `unstack` method.
-
-    `pivot_table` function is not used, because it is quite slow,
-    compared to the `pivot` function and `unstack`.
-
     By default, values from `names_from` are at the front of
-    each output column. If there are multiple `values_from`, this
-    can be changed via the `names_from_position`, by setting it to
-    `last`.
+    each output column. If there are multiple `values_from`,
+    this can be changed via the `names_from_position`,
+    by setting it to `last`.
 
-    The columns are sorted in the order of appearance from the
-    source data. This only occurs if `flatten_levels` is `True`.
-    It can be turned off by setting `names_sort` to `True`.
+    A dataframe is returned.
     """
 
-    if values_from is None:
-        if index:
-            values_from = [
-                col for col in df if col not in (index + names_from)
-            ]
-        else:
-            values_from = [col for col in df if col not in names_from]
-
-    dtypes = None
-    names_sort_and_flatten_levels = all(
-        (names_sort is False, flatten_levels is True)
-    )
-    if names_sort_and_flatten_levels:
-        # dtypes only needed for names_from
+    if not names_sort:
+        # Categorical dtypes created only for `names_from`
         # since that is what will become the new column names
         dtypes = {
             column_name: CategoricalDtype(
@@ -1679,74 +1688,72 @@ def _computations_pivot_wider(
             )
             if column.hasnans
             else CategoricalDtype(categories=column.unique(), ordered=True)
-            for column_name, column in df.loc[:, names_from].items()
+            for column_name, column in df.filter(names_from).items()
         }
 
         df = df.astype(dtypes)
 
-    if index is None:  # use existing index
-        df = df.set_index(names_from, append=True)
-    else:
-        df = df.set_index(index + names_from)
-
-    if (not df.index.is_unique) and (aggfunc is None):
-        raise ValueError(
-            """
-            There are non-unique values in your combination
-            of `index` and `names_from`. Kindly provide a
-            unique identifier for each row.
-            """
+    if aggfunc is None:
+        df = df.pivot(  # noqa: PD010
+            index=index, columns=names_from, values=values_from
         )
 
-    df = df.loc[:, values_from]
+    else:
+        if index:
+            df = df.set_index(index + names_from)
+        else:
+            df = df.set_index(names_from, append=True)
+
+        if values_from:
+            df = df.groupby(
+                level=[*range(df.index.nlevels)],
+                observed=True,
+                dropna=False,
+                sort=False,
+            )[values_from].agg(aggfunc)
+        else:
+            df = df.groupby(
+                level=[*range(df.index.nlevels)],
+                observed=True,
+                dropna=False,
+                sort=False,
+            ).agg(aggfunc)
 
-    if df.shape[-1] == 1:
-        df = df.iloc[:, 0]  # aligns with expected output if pd.pivot is used
+        df = df.unstack(level=names_from)  # noqa: PD010
 
-    aggfunc_index = None
-    if aggfunc is not None:
-        aggfunc_index = list(range(df.index.nlevels))
-        # since names_from may be categoricals if `names_sort`
-        # is False and `flatten_levels` is True.
-        # observed is set to True, keeping results consistent
-        if names_sort_and_flatten_levels:
-            df = df.groupby(level=aggfunc_index, observed=True).agg(aggfunc)
-        else:
-            df = df.groupby(level=aggfunc_index).agg(aggfunc)
+    if fill_value is not None:
+        df = df.fillna(fill_value)
 
-    df = df.unstack(level=names_from, fill_value=fill_value)  # noqa: PD010
+    # no point keeping `values_from`
+    # if it's just one name;
+    # could do same for aggfunc; but not worth the extra check
+    if df.columns.get_level_values(0).unique().size == 1:
+        df = df.droplevel(0, axis="columns")
+
+    other_levels = None
+    names_from_levels = None
+    df_columns = df.columns
+    df_columns_names = df_columns.names
+    if names_from_position == "first":
+        if df_columns_names[: len(names_from)] != names_from:
+            other_levels = [
+                num
+                for num, name in enumerate(df_columns_names)
+                if name not in names_from
+            ]
+            names_from_levels = [
+                num
+                for num, name in enumerate(df_columns_names)
+                if name in names_from
+            ]
+            df = df.reorder_levels(
+                names_from_levels + other_levels, axis="columns"
+            )
 
     if not flatten_levels:
         return df
 
-    extra_levels = df.columns.nlevels - len(names_from)
-    if extra_levels == 1:
-        if len(df.columns.get_level_values(0).unique()) == 1:
-            df = df.droplevel(level=0, axis="columns")
-        else:
-            df.columns = df.columns.set_names(level=0, names="values_from")
-    elif extra_levels == 2:
-        df.columns = df.columns.set_names(
-            level=[0, 1], names=["values_from", "aggfunc"]
-        )
-
-        if len(df.columns.get_level_values("aggfunc").unique()) == 1:
-            df = df.droplevel("aggfunc", axis="columns")
-
-    new_order_level = None
-    if (
-        df.columns.nlevels != len(names_from)
-        and names_from_position == "first"
-    ):
-        new_order_level = pd.Index(names_from).union(
-            df.columns.names, sort=False
-        )
-        df = df.reorder_levels(order=new_order_level, axis="columns")
-
-    if names_sort:
-        df = df.sort_index(axis="columns", level=names_from)
-
-    if df.columns.nlevels > 1:
+    if df_columns.nlevels > 1:
         df.columns = [names_sep.join(column_tuples) for column_tuples in df]
 
     if names_prefix:
@@ -1755,7 +1762,8 @@ def _computations_pivot_wider(
     # if columns are of category type
     # this returns columns to object dtype
     # also, resetting index with category columns is not possible
-    df.columns = list(df.columns)
+    if names_sort:
+        df.columns = [*df.columns]
 
     if index:
         df = df.reset_index()
@@ -1897,19 +1905,16 @@ def _computations_as_categorical(df: pd.DataFrame, **kwargs) -> pd.DataFrame:
 
 
 @functools.singledispatch
-def _select_columns(columns_to_select, df, level=None):
+def _select_columns(columns_to_select, df):
     """
     base function for column selection.
-    `level` parameter provided for selection in a MultiIndex.
-    Introduced primarily as a helper for `pivot_longer` function.
-    At the moment, not applied to the `select_columns` function,
-    since it is primarily focused on single indexed columns.
+    Returns a list of column names.
     """
     raise TypeError("This type is not supported in column selection.")
 
 
 @_select_columns.register(str)  # noqa: F811
-def _column_sel_dispatch(columns_to_select, df, level=None):  # noqa: F811
+def _column_sel_dispatch(columns_to_select, df):  # noqa: F811
     """
     Base function for column selection.
     Applies only to strings.
@@ -1918,25 +1923,20 @@ def _column_sel_dispatch(columns_to_select, df, level=None):  # noqa: F811
     A list of column names is returned.
     """
     filtered_columns = None
-    columns = None
-    if isinstance(df.columns, pd.MultiIndex):
-        if level is None:
-            raise ValueError("""Kindly provide a column level.""")
-        check("level", level, [str, int])
-        columns = df.columns.get_level_values(level)
-    else:
-        columns = df.columns
+    df_columns = df.columns
     if "*" in columns_to_select:  # shell-style glob string (e.g., `*_thing_*`)
-        filtered_columns = fnmatch.filter(columns, columns_to_select)
-    elif columns_to_select in columns:
+        filtered_columns = fnmatch.filter(df_columns, columns_to_select)
+    elif columns_to_select in df_columns:
         filtered_columns = [columns_to_select]
+        return filtered_columns
     if not filtered_columns:
         raise KeyError(f"No match was returned for '{columns_to_select}'")
+    df_columns = None
     return filtered_columns
 
 
 @_select_columns.register(slice)  # noqa: F811
-def _column_sel_dispatch(columns_to_select, df, level=None):  # noqa: F811
+def _column_sel_dispatch(columns_to_select, df):  # noqa: F811
     """
     Base function for column selection.
     Applies only to slices.
@@ -1949,16 +1949,21 @@ def _column_sel_dispatch(columns_to_select, df, level=None):  # noqa: F811
     A list of column names is returned.
     """
 
-    # for MultiIndex, if passed correctly,
-    # a slice returns a list of tuples.
-
+    df_columns = df.columns
     filtered_columns = None
     start_check = None
     stop_check = None
     step_check = None
-    # df.columns should be monotonic/unique,
-    # but we wont check for that
-    # onus is on the user to ensure that
+
+    if not df_columns.is_unique:
+        raise ValueError(
+            """
+            The column labels are not unique.
+            Kindly ensure the labels are unique
+            to ensure the correct output.
+            """
+        )
+
     start, stop, step = (
         columns_to_select.start,
         columns_to_select.stop,
@@ -1988,8 +1993,8 @@ def _column_sel_dispatch(columns_to_select, df, level=None):  # noqa: F811
             must either be an integer or `None`.
             """
         )
-    start_check = any((start is None, start in df.columns))
-    stop_check = any((stop is None, stop in df.columns))
+    start_check = any((start is None, start in df_columns))
+    stop_check = any((stop is None, stop in df_columns))
     if not start_check:
         raise ValueError(
             """
@@ -2004,22 +2009,32 @@ def _column_sel_dispatch(columns_to_select, df, level=None):  # noqa: F811
             or exist in the dataframe's columns.
             """
         )
-    filtered_columns = df.columns.slice_locs(start=start, end=stop)
-    # slice_locs fails when step has a value
-    # so this extra step is necessary to get the correct output
-    start, stop = filtered_columns
-    filtered_columns = df.columns[slice(start, stop, step)]
-    return list(filtered_columns)
+
+    if start is None:
+        start = 0
+    else:
+        start = df_columns.get_loc(start)
+    if stop is None:
+        stop = len(df_columns) + 1
+    else:
+        stop = df_columns.get_loc(stop)
+
+    # allows for reverse selection - write test for this
+    if start > stop:
+        filtered_columns = df_columns[slice(stop, start + 1, step)][::-1]
+    else:
+        filtered_columns = df_columns[slice(start, stop + 1, step)]
+    df_columns = None
+    return [*filtered_columns]
 
 
 @_select_columns.register(dispatch_callable)  # noqa: F811
-def _column_sel_dispatch(columns_to_select, df, level=None):  # noqa: F811
+def _column_sel_dispatch(columns_to_select, df):  # noqa: F811
     """
     Base function for column selection.
     Applies only to callables.
     The callable is applied to every column in the dataframe.
     Either True or False is expected per column.
-    Not implemented for MultiIndex columns.
     A list of column names is returned.
     """
     # the function will be applied per series.
@@ -2030,35 +2045,16 @@ def _column_sel_dispatch(columns_to_select, df, level=None):  # noqa: F811
     # the returned values should be a sequence of booleans,
     # with at least one True.
 
-    if isinstance(df.columns, pd.MultiIndex):
+    filtered_columns = df.agg(columns_to_select)
+
+    if not filtered_columns.any():
         raise ValueError(
             """
-            MultiIndex columns not applicable
-            for callables in column selection.
+            No match was returned for the provided callable.
             """
         )
-    filtered_columns = None
-    filtered_columns = [columns_to_select(column) for _, column in df.items()]
-
-    # returns numpy bool,
-    # which does not work the same way as python's bool
-    # as such, cant use isinstance.
-    # pandas type check function helps out
-    checks = (pd.api.types.is_bool(column) for column in filtered_columns)
-
-    if not all(checks):
-        raise ValueError(
-            "The callable provided must return a sequence of booleans."
-        )
-
-    # cant use `is` here, since it may be a numpy bool
-    checks = any((column == True for column in filtered_columns))  # noqa: E712
 
-    if not checks:
-        raise ValueError("No results were returned for the callable provided.")
-
-    filtered_columns = df.columns[filtered_columns]
-    return list(filtered_columns)
+    return [*df.columns[filtered_columns]]
 
 
 # hack to get it to recognize typing.Pattern
@@ -2070,60 +2066,47 @@ def _column_sel_dispatch(columns_to_select, df, level=None):  # noqa: F811
 # however, the same type from typing.Pattern
 # is not accepted.
 @_select_columns.register(type(re.compile(r"\d+")))  # noqa: F811
-def _column_sel_dispatch(columns_to_select, df, level=None):  # noqa: F811
+def _column_sel_dispatch(columns_to_select, df):  # noqa: F811
     """
     Base function for column selection.
     Applies only to regular expressions.
     `re.compile` is required for the regular expression.
     A list of column names is returned.
     """
-    filtered_columns = None
-    columns = None
-    if isinstance(df.columns, pd.MultiIndex):
-        if level is None:
-            raise ValueError("""Kindly provide a column level.""")
-        check("level", level, [str, int])
-        columns = df.columns.get_level_values(level)
-    else:
-        columns = df.columns
-
+    df_columns = df.columns
     filtered_columns = [
         column_name
-        for column_name in columns
+        for column_name in df_columns
         if re.search(columns_to_select, column_name)
     ]
 
     if not filtered_columns:
         raise KeyError("No column name matched the regular expression.")
+    df_columns = None
 
     return filtered_columns
 
 
 @_select_columns.register(tuple)  # noqa: F811
-def _column_sel_dispatch(columns_to_select, df, level=None):  # noqa: F811
+def _column_sel_dispatch(columns_to_select, df):  # noqa: F811
     """
     Base function for column selection.
-    Applies only to tuple type, and extends to MultiIndex columns.
-    A list of column names is returned.
-    The `level` parameter is not applied for tuples.
+    This caters to columns that are of tuple type.
+    The tuple is returned as is, if it exists in the columns.
     """
-    if isinstance(df.columns, pd.MultiIndex):
-        if columns_to_select not in df.columns:
-            raise KeyError(f"No match was returned for {columns_to_select}")
-        return columns_to_select
-    columns_to_select = list(columns_to_select)
-    return _select_columns(columns_to_select, df)
+    if columns_to_select not in df.columns:
+        raise KeyError(f"No match was returned for {columns_to_select}")
+    return columns_to_select
 
 
 @_select_columns.register(list)  # noqa: F811
-def _column_sel_dispatch(columns_to_select, df, level=None):  # noqa: F811
+def _column_sel_dispatch(columns_to_select, df):  # noqa: F811
     """
     Base function for column selection.
     Applies only to list type.
     It can take any of slice, str, callable, re.Pattern types,
     or a combination of these types.
-    A tuple of column names is returned, if the columns is
-    a MultiIndex, else a list is returned.
+    A tuple of column names is returned.
     """
 
     # takes care of boolean entries
@@ -2136,18 +2119,17 @@ def _column_sel_dispatch(columns_to_select, df, level=None):  # noqa: F811
                 in the dataframe.
                 """
             )
-        # boolean applied to all levels if a MultiIndex
-        return df.columns[columns_to_select].tolist()
+
+        return [*df.columns[columns_to_select]]
 
     filtered_columns = []
     columns_to_select = (
-        _select_columns(entry, df, level=level) for entry in columns_to_select
+        _select_columns(entry, df) for entry in columns_to_select
     )
 
-    # this is required, as the next code uses the `extend` method for lists
-    # it allows `tuple` status to be maintained
-    # especially for `pivot_longer` that requires list of tuples
-    # when melting a MultiIndex column.
+    # this is required,
+    # to maintain `tuple` status
+    # when combining all the entries into a single list
     columns_to_select = (
         [entry] if isinstance(entry, tuple) else entry
         for entry in columns_to_select
@@ -2163,16 +2145,14 @@ def _column_sel_dispatch(columns_to_select, df, level=None):  # noqa: F811
     return filtered_columns
 
 
-# implement dispatch for process_text
 @functools.singledispatch
 def _process_text(result: str, df, column_name, new_column_names, merge_frame):
     """
     Base function for `process_text` when `result` is of ``str`` type.
     """
     if new_column_names:
-        df.loc[:, new_column_names] = result
-    else:
-        df.loc[:, column_name] = result
+        return df.assign(**{new_column_names: result})
+    df[column_name] = result
     return df
 
 
@@ -2184,9 +2164,8 @@ def _sub_process_text(
     Base function for `process_text` when `result` is of ``pd.Series`` type.
     """
     if new_column_names:
-        df.loc[:, new_column_names] = result
-    else:
-        df.loc[:, column_name] = result
+        return df.assign(**{new_column_names: result})
+    df[column_name] = result
     return df
 
 


Test patch:
diff --git a/tests/functions/test_complete.py b/tests/functions/test_complete.py
--- a/tests/functions/test_complete.py
+++ b/tests/functions/test_complete.py
@@ -42,7 +42,7 @@ def test_column_duplicated(df1):
             columns=[
                 "Year",
                 "Taxon",
-                {"Year": lambda x: range(x.Year.min().x.Year.max() + 1)},
+                {"Year": lambda x: range(x.min().x.max() + 1)},
             ]
         )
 
@@ -53,16 +53,18 @@ def test_type_columns(df1):
         df1.complete(columns="Year")
 
 
+@pytest.mark.xfail(reason="fill_value dropped. fillna better.")
 def test_fill_value_is_a_dict(df1):
     """Raise error if fill_value is not a dictionary"""
     with pytest.raises(TypeError):
-        df1.complete(columns=["Year", "Taxon"], fill_value=0)
+        df1.complete(columns=["Year", "Taxon"])
 
 
+@pytest.mark.xfail(reason="fill_value dropped. fillna better.")
 def test_wrong_column_fill_value(df1):
     """Raise ValueError if column in `fill_value` does not exist."""
     with pytest.raises(ValueError):
-        df1.complete(columns=["Taxon", "Year"], fill_value={"year": 0})
+        df1.complete(columns=["Taxon", "Year"])
 
 
 def test_wrong_data_type_dict(df1):
@@ -74,6 +76,37 @@ def test_wrong_data_type_dict(df1):
         df1.complete(columns=[{"Year": pd.DataFrame([2005, 2006, 2007])}])
 
 
+def test_not_list_like_type_dict(df1):
+    """
+    Raise ValueError if value in dictionary
+    is not a list-like object.
+    """
+    with pytest.raises(ValueError):
+        df1.complete(columns=[{"Year": "2001, 2002, 2003"}])
+
+
+def test_MultiIndex_type_dict(df1):
+    """
+    Raise ValueError if value in dictionary
+    is a MultiIndex.
+    """
+    with pytest.raises(ValueError):
+        df1.complete(
+            columns=[
+                {"Year": pd.MultiIndex.from_tuples([(1, 2001), (2, 2002)])}
+            ]
+        )
+
+
+def test_empty_type_dict(df1):
+    """
+    Raise ValueError if value in dictionary
+    is empty.
+    """
+    with pytest.raises(ValueError):
+        df1.complete(columns=[{"Year": pd.Index([])}])
+
+
 frame = pd.DataFrame(
     {
         "Year": [1999, 2000, 2004, 1999, 2004],
@@ -115,6 +148,7 @@ def test_empty_subcols(frame, empty_sub_cols):
         frame.complete(columns=empty_sub_cols)
 
 
+@pytest.mark.xfail(reason="fill_value dropped. fillna is better.")
 def test_fill_value(df1):
     """Test fill_value argument."""
     output1 = pd.DataFrame(
@@ -132,9 +166,7 @@ def test_fill_value(df1):
         }
     )
 
-    result = df1.complete(
-        columns=["Year", "Taxon"], fill_value={"Abundance": 0}
-    )
+    result = df1.complete(columns=["Year", "Taxon"]).fillna({"Abundance": 0})
     assert_frame_equal(result, output1)
 
 
@@ -182,12 +214,8 @@ def test_fill_value_all_years(df1, df1_output):
     """
 
     result = df1.complete(
-        columns=[
-            {"Year": lambda x: range(x.Year.min(), x.Year.max() + 1)},
-            "Taxon",
-        ],
-        fill_value={"Abundance": 0},
-    )
+        columns=[{"Year": lambda x: range(x.min(), x.max() + 1)}, "Taxon"]
+    ).fillna(0)
     assert_frame_equal(result, df1_output)
 
 
@@ -199,15 +227,10 @@ def test_dict_series(df1, df1_output):
 
     result = df1.complete(
         columns=[
-            {
-                "Year": lambda x: pd.Series(
-                    range(x.Year.min(), x.Year.max() + 1)
-                )
-            },
+            {"Year": lambda x: pd.Series(range(x.min(), x.max() + 1))},
             "Taxon",
-        ],
-        fill_value={"Abundance": 0},
-    )
+        ]
+    ).fillna(0)
     assert_frame_equal(result, df1_output)
 
 
@@ -225,9 +248,8 @@ def test_dict_series_duplicates(df1, df1_output):
                 )
             },
             "Taxon",
-        ],
-        fill_value={"Abundance": 0},
-    )
+        ]
+    ).fillna(0)
     assert_frame_equal(result, df1_output)
 
 
@@ -264,8 +286,204 @@ def test_dict_values_outside_range(df1):
     assert_frame_equal(result, expected)
 
 
-# adapted from https://tidyr.tidyverse.org/reference/complete.html
 complete_parameters = [
+    (
+        pd.DataFrame(
+            [
+                {
+                    "date": pd.Timestamp("2014-10-20 00:00:00"),
+                    "colour": "red",
+                    "orders": 7,
+                },
+                {
+                    "date": pd.Timestamp("2014-10-21 00:00:00"),
+                    "colour": "red",
+                    "orders": 10,
+                },
+                {
+                    "date": pd.Timestamp("2014-10-20 00:00:00"),
+                    "colour": "yellow",
+                    "orders": 3,
+                },
+            ]
+        ),
+        [{"date": pd.date_range("2014-10-20", "2014-10-22")}, "colour"],
+        pd.DataFrame(
+            [
+                {
+                    "date": pd.Timestamp("2014-10-20 00:00:00"),
+                    "colour": "red",
+                    "orders": 7.0,
+                },
+                {
+                    "date": pd.Timestamp("2014-10-20 00:00:00"),
+                    "colour": "yellow",
+                    "orders": 3.0,
+                },
+                {
+                    "date": pd.Timestamp("2014-10-21 00:00:00"),
+                    "colour": "red",
+                    "orders": 10.0,
+                },
+                {
+                    "date": pd.Timestamp("2014-10-21 00:00:00"),
+                    "colour": "yellow",
+                    "orders": np.nan,
+                },
+                {
+                    "date": pd.Timestamp("2014-10-22 00:00:00"),
+                    "colour": "red",
+                    "orders": np.nan,
+                },
+                {
+                    "date": pd.Timestamp("2014-10-22 00:00:00"),
+                    "colour": "yellow",
+                    "orders": np.nan,
+                },
+            ]
+        ),
+    ),
+    (
+        pd.DataFrame(
+            {
+                "id": [1, 2, 3],
+                "choice": [5, 6, 7],
+                "c": [9.0, np.nan, 11.0],
+                "d": [
+                    pd.NaT,
+                    pd.Timestamp("2015-09-30 00:00:00"),
+                    pd.Timestamp("2015-09-29 00:00:00"),
+                ],
+            }
+        ),
+        ["id", "c"],
+        pd.DataFrame(
+            [
+                {"id": 1, "c": 9.0, "choice": 5.0, "d": pd.NaT},
+                {"id": 1, "c": np.nan, "choice": np.nan, "d": pd.NaT},
+                {"id": 1, "c": 11.0, "choice": np.nan, "d": pd.NaT},
+                {"id": 2, "c": 9.0, "choice": np.nan, "d": pd.NaT},
+                {
+                    "id": 2,
+                    "c": np.nan,
+                    "choice": 6.0,
+                    "d": pd.Timestamp("2015-09-30 00:00:00"),
+                },
+                {"id": 2, "c": 11.0, "choice": np.nan, "d": pd.NaT},
+                {"id": 3, "c": 9.0, "choice": np.nan, "d": pd.NaT},
+                {"id": 3, "c": np.nan, "choice": np.nan, "d": pd.NaT},
+                {
+                    "id": 3,
+                    "c": 11.0,
+                    "choice": 7.0,
+                    "d": pd.Timestamp("2015-09-29 00:00:00"),
+                },
+            ]
+        ),
+    ),
+    (
+        pd.DataFrame(
+            {
+                "row": {
+                    0: "22.08.2020",
+                    1: "22.08.2020",
+                    2: "21.08.2020",
+                    3: "21.08.2020",
+                    4: "21.08.2020",
+                    5: "21.08.2020",
+                    6: "22.08.2020",
+                },
+                "column": {
+                    0: "B",
+                    1: "B",
+                    2: "A",
+                    3: "A",
+                    4: "B",
+                    5: "C",
+                    6: "A",
+                },
+                "value": {0: 40, 1: 34, 2: 43, 3: 36, 4: 36, 5: 28, 6: 16},
+            }
+        ),
+        ["row", "column"],
+        pd.DataFrame(
+            [
+                {"row": "21.08.2020", "column": "A", "value": 43.0},
+                {"row": "21.08.2020", "column": "A", "value": 36.0},
+                {"row": "21.08.2020", "column": "B", "value": 36.0},
+                {"row": "21.08.2020", "column": "C", "value": 28.0},
+                {"row": "22.08.2020", "column": "A", "value": 16.0},
+                {"row": "22.08.2020", "column": "B", "value": 40.0},
+                {"row": "22.08.2020", "column": "B", "value": 34.0},
+                {"row": "22.08.2020", "column": "C", "value": np.nan},
+            ]
+        ),
+    ),
+    (
+        pd.DataFrame(
+            {
+                "row": {
+                    0: "21.08.2020",
+                    1: "21.08.2020",
+                    2: "21.08.2020",
+                    3: "21.08.2020",
+                    4: "22.08.2020",
+                    5: "22.08.2020",
+                    6: "22.08.2020",
+                },
+                "column": {
+                    0: "A",
+                    1: "A",
+                    2: "B",
+                    3: "C",
+                    4: "A",
+                    5: "B",
+                    6: "B",
+                },
+                "value": {0: 43, 1: 36, 2: 36, 3: 28, 4: 16, 5: 40, 6: 34},
+            }
+        ),
+        ["row", "column"],
+        pd.DataFrame(
+            [
+                {"row": "21.08.2020", "column": "A", "value": 43.0},
+                {"row": "21.08.2020", "column": "A", "value": 36.0},
+                {"row": "21.08.2020", "column": "B", "value": 36.0},
+                {"row": "21.08.2020", "column": "C", "value": 28.0},
+                {"row": "22.08.2020", "column": "A", "value": 16.0},
+                {"row": "22.08.2020", "column": "B", "value": 40.0},
+                {"row": "22.08.2020", "column": "B", "value": 34.0},
+                {"row": "22.08.2020", "column": "C", "value": np.nan},
+            ]
+        ),
+    ),
+    (
+        pd.DataFrame(
+            [
+                {"YEAR": 1946, "Region": 1},
+                {"YEAR": 1946, "Region": 2},
+                {"YEAR": 1946, "Region": 3},
+                {"YEAR": 1946, "Region": 5},
+                {"YEAR": 1947, "Region": 3},
+                {"YEAR": 1947, "Region": 4},
+            ]
+        ),
+        ["YEAR", "Region"],
+        pd.DataFrame(
+            [
+                {"YEAR": 1946, "Region": 1},
+                {"YEAR": 1946, "Region": 2},
+                {"YEAR": 1946, "Region": 3},
+                {"YEAR": 1946, "Region": 4},
+                {"YEAR": 1946, "Region": 5},
+                {"YEAR": 1947, "Region": 1},
+                {"YEAR": 1947, "Region": 2},
+                {"YEAR": 1947, "Region": 3},
+                {"YEAR": 1947, "Region": 4},
+                {"YEAR": 1947, "Region": 5},
+            ]
+        ),
+    ),
     (
         pd.DataFrame(
             {
@@ -335,84 +553,6 @@ def test_complete(df, columns, output):
     assert_frame_equal(df.complete(columns), output)
 
 
-@pytest.fixture
-def duplicates():
-    return pd.DataFrame(
-        {
-            "row": [
-                "21.08.2020",
-                "21.08.2020",
-                "21.08.2020",
-                "21.08.2020",
-                "22.08.2020",
-                "22.08.2020",
-                "22.08.2020",
-                "22.08.2020",
-            ],
-            "column": ["A", "A", "B", "C", "A", "B", "B", "C"],
-            "value": [43.0, 36, 36, 28, 16, 40, 34, 0],
-        }
-    )
-
-
-# https://stackoverflow.com/questions/63541729/
-# pandas-how-to-include-all-columns-for-all-rows-although-value-is-missing-in-a-d
-# /63543164#63543164
-def test_duplicates(duplicates):
-    """Test that the complete function works for duplicate values."""
-    df = pd.DataFrame(
-        {
-            "row": {
-                0: "21.08.2020",
-                1: "21.08.2020",
-                2: "21.08.2020",
-                3: "21.08.2020",
-                4: "22.08.2020",
-                5: "22.08.2020",
-                6: "22.08.2020",
-            },
-            "column": {0: "A", 1: "A", 2: "B", 3: "C", 4: "A", 5: "B", 6: "B"},
-            "value": {0: 43, 1: 36, 2: 36, 3: 28, 4: 16, 5: 40, 6: 34},
-        }
-    )
-
-    result = df.complete(columns=["row", "column"], fill_value={"value": 0})
-
-    assert_frame_equal(result, duplicates)
-
-
-def test_unsorted_duplicates(duplicates):
-    """Test output for unsorted duplicates."""
-
-    df = pd.DataFrame(
-        {
-            "row": {
-                0: "22.08.2020",
-                1: "22.08.2020",
-                2: "21.08.2020",
-                3: "21.08.2020",
-                4: "21.08.2020",
-                5: "21.08.2020",
-                6: "22.08.2020",
-            },
-            "column": {
-                0: "B",
-                1: "B",
-                2: "A",
-                3: "A",
-                4: "B",
-                5: "C",
-                6: "A",
-            },
-            "value": {0: 40, 1: 34, 2: 43, 3: 36, 4: 36, 5: 28, 6: 16},
-        }
-    )
-
-    result = df.complete(columns=["row", "column"], fill_value={"value": 0})
-
-    assert_frame_equal(result, duplicates)
-
-
 # https://stackoverflow.com/questions/32874239/
 # how-do-i-use-tidyr-to-fill-in-completed-rows-within-each-value-of-a-grouping-var
 def test_grouping_first_columns():
@@ -480,8 +620,7 @@ def test_complete_multiple_groupings():
 
     result = df3.complete(
         columns=[("meta", "domain1"), ("project_id", "question_count")],
-        fill_value={"tag_count": 0},
-    )
+    ).fillna({"tag_count": 0})
     assert_frame_equal(result, output3)
 
 
@@ -531,7 +670,7 @@ def test_dict_tuple(df1, output_dict_tuple):
 
     result = df1.complete(
         columns=[
-            {"Year": lambda x: range(x.Year.min(), x.Year.max() + 1)},
+            {"Year": lambda x: range(x.min(), x.max() + 1)},
             ("Taxon", "Abundance"),
         ]
     )
@@ -550,8 +689,7 @@ def test_complete_groupby():
     )
 
     result = df.complete(
-        columns=[{"year": lambda x: range(x.year.min(), x.year.max() + 1)}],
-        by="state",
+        columns=[{"year": lambda x: range(x.min(), x.max() + 1)}], by="state",
     )
 
     expected = pd.DataFrame(
diff --git a/tests/functions/test_expand_grid.py b/tests/functions/test_expand_grid.py
--- a/tests/functions/test_expand_grid.py
+++ b/tests/functions/test_expand_grid.py
@@ -254,6 +254,7 @@ def test_expand_grid_others_only(grid_input, grid_output):
     assert_frame_equal(expand_grid(others=grid_input), grid_output)
 
 
+@pytest.mark.xfail(reason="Indexes now converted to Series/DataFrame.")
 def test_not_accepted_type():
     """Raise TypeError if wrong data type is used in `others`."""
     others = {
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
@@ -88,8 +88,8 @@ def df_multi():
     )
 
 
-index_labels = [pd.Index(["region"]), {"2007", "region"}]
-column_labels = [{"region": 2007}, {"2007", "2009"}]
+index_labels = [2007, 20.09]
+column_labels = [2007, 20.99]
 names_to_labels = [1, {12, "newnames"}]
 
 index_does_not_exist = ["Region", ["207", "region"]]
@@ -136,7 +136,6 @@ def df_multi():
 ]
 
 
-@pytest.mark.xfail(reason="list-like converted to lists.")
 @pytest.mark.parametrize("df,index", index_type_checks)
 def test_type_index(df, index):
     """Raise TypeError if wrong type is provided for the `index`."""
@@ -144,7 +143,6 @@ def test_type_index(df, index):
         df.pivot_longer(index=index)
 
 
-@pytest.mark.xfail(reason="list-like converted to list.")
 @pytest.mark.parametrize("df,column", column_type_checks)
 def test_type_column_names(df, column):
     """Raise TypeError if wrong type is provided for `column_names`."""
@@ -176,11 +174,6 @@ def test_presence_index(df, index):
         df.pivot_longer(index=index)
 
 
-@pytest.mark.xfail(
-    reason="""
-           First test will fail on TypeError instead since
-           `_select_columns` does not support integers"""
-)
 @pytest.mark.parametrize("df,column", column_presence_checks)
 def test_presence_columns(df, column):
     """Raise KeyError if labels in `column_names` do not exist."""
@@ -191,8 +184,10 @@ def test_presence_columns(df, column):
 @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 the `names_to` is a string, or `names_to` is a
-    list/tuple, its length is one, and `names_sep` is provided.
+    Raise ValueError if `names_to` is a string,
+    or `names_to` is a list/tuple,
+    its length is one,
+    and `names_sep` is provided.
     """
     with pytest.raises(ValueError):
         df.pivot_longer(names_to=names_to, names_sep=names_sep)
@@ -304,19 +299,25 @@ def test_both_names_sep_and_pattern():
 def test_names_pattern_column_MultiIndex(df_multi):
     """Raise ValueError if `names_pattern` and MultiIndex column"""
     with pytest.raises(ValueError):
-        df_multi.pivot_longer(
-            index="name", column_level=0, names_pattern=r"(.+)(.)"
-        )
+        df_multi.pivot_longer(index="name", names_pattern=r"(.+)(.)")
 
 
 def test_index_tuple_MultiIndex(df_multi):
-    """Raise ValueError if `index` is a tuple and MultiIndex column"""
+    """
+    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_multi.pivot_longer(index=("name", "a"))
 
 
 def test_column_names_tuple_MultiIndex(df_multi):
-    """Raise ValueError if `column_names` is a tuple and MultiIndex column"""
+    """
+    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_multi.pivot_longer(column_names=("names", "aa"))
 
@@ -474,8 +475,8 @@ def test_pivot_column_only(df_checks_output):
 
 def test_pivot_index_patterns_only_sort_by_appearance(df_checks_output):
     """
-    Test output if the `patterns` function is passed to `index`, and
-    `sort_by_appearance` is ``True``.
+    Test output if the `patterns` function is passed to `index`,
+    and `sort_by_appearance` is ``True``.
     """
     result = df_checks.pivot_longer(
         index=patterns(r"[^\d+]"),
@@ -514,8 +515,9 @@ def test_pivot_columns_patterns_only(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.
+    Raise error if `names_to` is a list/tuple,
+    and its length  does not match
+    the number of extracted columns.
     """
     data = pd.DataFrame(
         {
@@ -534,7 +536,7 @@ def test_length_mismatch():
         data.pivot_longer(names_to=["event", "year"], names_sep="-")
 
 
-def test_empty_mapping(test_df):
+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(
@@ -542,10 +544,22 @@ def test_empty_mapping(test_df):
         )
 
 
+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)"
+        )
+
+
 def test_len_mapping_gt_len_names_to(test_df):
     """
-    Raise error if `names_pattern` is a regex and returns number of
-    matches more than length of `names_to`.
+    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(
@@ -573,7 +587,7 @@ def names_pattern_list_df():
     )
 
 
-def test_names_pattern_list_empty(names_pattern_list_df):
+def test_names_pattern_list_empty_all(names_pattern_list_df):
     """
     Raise ValueError if `names_pattern` is a list,
     and nothing is returned.
@@ -586,6 +600,19 @@ def test_names_pattern_list_empty(names_pattern_list_df):
         )
 
 
+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$"),
+        )
+
+
 def test_names_pattern_list(names_pattern_list_df):
     """Test output if `names_pattern` is a list."""
     result = names_pattern_list_df.pivot_longer(
@@ -711,8 +738,8 @@ 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.
+    Test function to extract multiple columns,
+    using the `names_to` and `names_pattern` arguments.
     """
     result = df_in.pivot_longer(
         index=index,
@@ -1026,8 +1053,8 @@ 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.
+    Test function to extract multiple columns,
+    using the `names_to` and `names_sep` arguments.
     """
     result = df_in.pivot_longer(
         index=index,
@@ -1130,7 +1157,8 @@ def test_multiple_values_sep(
                 "instance": ["1", "2", "3"],
                 "a": ["a", "b", "c"],
                 "A": ["A", "B", "C"],
-            }
+            },
+            index=[0, 0, 0],
         ),
         "id",
         (".value", "instance"),
@@ -1227,7 +1255,7 @@ def test_extract_column_names_pattern(
 ):
     """
     Test output if `.value` is in the `names_to`
-    argument and names_pattern is used.
+    argument and `names_pattern` is used.
     """
     result = df_in.pivot_longer(
         index=index,
@@ -1571,7 +1599,7 @@ def test_extract_column_names_sep(
 ):
     """
     Test output if `.value` is in the `names_to` argument
-    and names_sep is used.
+    and `names_sep` is used.
     """
     result = df_in.pivot_longer(
         index=index,
@@ -1702,7 +1730,8 @@ def test_paired_columns_no_index_pattern(
 ):
     """
     Test function where `.value` is in the `names_to` argument,
-    names_pattern is used and no index is supplied.
+    names_pattern is used,
+    and no `index` is supplied.
     """
     result = df_in.pivot_longer(
         names_to=names_to,
@@ -1884,7 +1913,7 @@ def test_single_column_names_pattern(
     Test output if `names_to` is a string and
     `names_pattern` returns a single column.
     Also tests when both `index` and `column_names`
-    is supplied, and only a subset of the dataframe
+    are supplied, and only a subset of the dataframe
     is transformed.
     """
     result = df_in.pivot_longer(
@@ -1897,6 +1926,9 @@ def test_single_column_names_pattern(
     assert_frame_equal(result, df_out)
 
 
+# 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(
@@ -1946,11 +1978,12 @@ def test_float_suffix_irregular():
     expected = pd.DataFrame(
         {
             "A": ["X1", "X1", "X1", "X1", "X2", "X2", "X2", "X2"],
-            "colname": ["1", "1.1", "1.2", "2.1", "1", "1.1", "1.2", "2.1"],
-            "treatment": [np.nan, 1.0, np.nan, 3.0, np.nan, 2.0, np.nan, 4.0],
-            "result": [0.0, np.nan, 5.0, np.nan, 9.0, np.nan, 6.0, np.nan],
+            "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 = df.pivot_longer(
         index="A",
         names_to=(".value", "colname"),
@@ -1958,3 +1991,231 @@ def test_float_suffix_irregular():
         sort_by_appearance=True,
     )
     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):
+    """
+    Test output for scenario where the pairing
+    in the column name is more than 2.
+    """
+
+    result = df_in.pivot_longer(
+        names_to=("Manufacturer", "Device", ".value"),
+        names_pattern=r"(.+)\|(.+)\|(.+)\|.*",
+    )
+
+    assert_frame_equal(result, df_out)
diff --git a/tests/functions/test_pivot_wider.py b/tests/functions/test_pivot_wider.py
--- a/tests/functions/test_pivot_wider.py
+++ b/tests/functions/test_pivot_wider.py
@@ -74,6 +74,7 @@ def df_checks_output():
         ["variable", "measure"],
         "value",
         None,
+        True,
         "first",
     ),
     (
@@ -95,6 +96,7 @@ def df_checks_output():
         "n",
         "name",
         None,
+        True,
         "first",
     ),
     (
@@ -116,6 +118,7 @@ def df_checks_output():
         "n",
         "name",
         "name",
+        True,
         "first",
     ),
     (
@@ -147,6 +150,7 @@ def df_checks_output():
         "variable",
         ["estimate", "error"],
         None,
+        False,
         "first",
     ),
     (
@@ -178,6 +182,7 @@ def df_checks_output():
         "variable",
         ["estimate", "error"],
         None,
+        False,
         "last",
     ),
 ]
@@ -359,7 +364,8 @@ def test_pivot_long_wide_long():
     assert_frame_equal(result, df_in)
 
 
-def pivot_wide_long_wide():
+@pytest.mark.xfail(reason="doesnt match, since pivot implicitly sorts")
+def test_pivot_wide_long_wide():
     """
     Test that transformation from pivot_longer to wider and
     back to longer returns the same source dataframe.
@@ -386,7 +392,8 @@ def pivot_wide_long_wide():
 @pytest.mark.parametrize(
     """
     df_in,df_out,index,names_from,
-    values_from, names_prefix,names_from_position
+    values_from, names_prefix,
+    names_sort,names_from_position
     """,
     combinations,
 )
@@ -397,6 +404,7 @@ def test_pivot_wider_various(
     names_from,
     values_from,
     names_prefix,
+    names_sort,
     names_from_position,
 ):
     """
@@ -407,6 +415,7 @@ def test_pivot_wider_various(
         names_from=names_from,
         values_from=values_from,
         names_prefix=names_prefix,
+        names_sort=names_sort,
         names_from_position=names_from_position,
     )
     assert_frame_equal(result, df_out)
@@ -430,6 +439,7 @@ def test_flatten_levels_false():
         values_from=["baz", "zoo"],
         flatten_levels=False,
         names_sort=True,
+        names_from_position="last",
     )
 
     expected_output = df_collapse.pivot(  # noqa: PD010
@@ -463,7 +473,7 @@ def test_fill_values():
     )
 
     expected_output = pd.DataFrame(
-        {1: [0, 2, 4, 0], 2: [1, 0, 3, 5]},
+        {1: [0, 2.0, 4, 0], 2: [1, 0, 3.0, 5]},
         index=pd.MultiIndex.from_tuples(
             [(1, 1), (1, 2), (2, 1), (2, 2)], names=["lev1", "lev2"]
         ),
@@ -484,8 +494,8 @@ def test_no_index():
 
     expected_output = pd.DataFrame(
         {
-            "contVar_Male": [22379.0, 23831.0, 29234.0],
             "contVar_Female": [24523.0, 23421.0, np.nan],
+            "contVar_Male": [22379.0, 23831.0, 29234.0],
         }
     )
 
@@ -643,7 +653,7 @@ def test_aggfunc_names_sort(df_aggfunc):
 def test_aggfunc_list(df_aggfunc):
     """Test output when `aggfunc` is a list."""
     expected = pd.DataFrame(
-        {"V4": ["A", "B", "C"], "V1": [0, 9, 6], "V2": [14, 13, 14]}
+        {"V4": ["A", "B", "C"], "V1_sum": [0, 9, 6], "V2_sum": [14, 13, 14]}
     )
     result = df_aggfunc.pivot_wider(
         index="V4", names_from="variable", aggfunc=["sum"], flatten_levels=True
@@ -657,10 +667,10 @@ def test_aggfunc_multiple_names_from(df_aggfunc_multiple_names_from):
         {
             "A": ["bar", "bar", "foo", "foo"],
             "C": ["large", "small", "large", "small"],
-            "one_D": [4.0, 5.0, 2.0, 1.0],
-            "two_D": [7.0, 6.0, np.nan, 3.0],
-            "one_E": [6.0, 8.0, 4.5, 2.0],
-            "two_E": [9.0, 9.0, np.nan, 5.5],
+            "one_D_mean": [4.0, 5.0, 2.0, 1.0],
+            "two_D_mean": [7.0, 6.0, np.nan, 3.0],
+            "one_E_mean": [6.0, 8.0, 4.5, 2.0],
+            "two_E_mean": [9.0, 9.0, np.nan, 5.5],
         }
     )
     result = df_aggfunc_multiple_names_from.pivot_wider(
@@ -712,16 +722,69 @@ def test_df_multiple_aggfuncs():
     assert_frame_equal(result, expected)
 
 
-df = pd.DataFrame(
-    [
-        {"A": "foo", "B": "one", "C": "small", "D": 1, "E": 2},
-        {"A": "foo", "B": "one", "C": "large", "D": 2, "E": 4},
-        {"A": "foo", "B": "one", "C": "large", "D": 2, "E": 5},
-        {"A": "foo", "B": "one", "C": "small", "D": 3, "E": 5},
-        {"A": "foo", "B": "one", "C": "small", "D": 3, "E": 6},
-        {"A": "bar", "B": "one", "C": "large", "D": 4, "E": 6},
-        {"A": "bar", "B": "one", "C": "small", "D": 5, "E": 8},
-        {"A": "bar", "B": "one", "C": "small", "D": 6, "E": 9},
-        {"A": "bar", "B": "one", "C": "large", "D": 7, "E": 9},
-    ]
-)
+def test_df_aggfunc_no_index():
+    """Test output when for `aggfunc` if no `index is provided."""
+
+    df = pd.DataFrame(
+        [
+            {"A": "foo", "B": "one", "C": "small", "D": 1, "E": 2},
+            {"A": "foo", "B": "one", "C": "large", "D": 2, "E": 4},
+            {"A": "foo", "B": "one", "C": "large", "D": 2, "E": 5},
+            {"A": "foo", "B": "one", "C": "small", "D": 3, "E": 5},
+            {"A": "foo", "B": "one", "C": "small", "D": 3, "E": 6},
+            {"A": "bar", "B": "one", "C": "large", "D": 4, "E": 6},
+            {"A": "bar", "B": "one", "C": "small", "D": 5, "E": 8},
+            {"A": "bar", "B": "one", "C": "small", "D": 6, "E": 9},
+            {"A": "bar", "B": "one", "C": "large", "D": 7, "E": 9},
+        ]
+    )
+
+    df = df.set_index(["A", "C"])
+
+    expected = pd.DataFrame(
+        [
+            {
+                "one_D_mean": 5.5,
+                "one_D_sum": 11,
+                "one_E_mean": 7.5,
+                "one_E_sum": 15,
+            },
+            {
+                "one_D_mean": 5.5,
+                "one_D_sum": 11,
+                "one_E_mean": 8.5,
+                "one_E_sum": 17,
+            },
+            {
+                "one_D_mean": 2.0,
+                "one_D_sum": 4,
+                "one_E_mean": 4.5,
+                "one_E_sum": 9,
+            },
+            {
+                "one_D_mean": 2.3333333333333335,
+                "one_D_sum": 7,
+                "one_E_mean": 4.333333333333333,
+                "one_E_sum": 13,
+            },
+        ],
+        index=pd.MultiIndex.from_tuples(
+            [
+                ("bar", "large"),
+                ("bar", "small"),
+                ("foo", "large"),
+                ("foo", "small"),
+            ],
+            names=["A", "C"],
+        ),
+    )
+
+    result = df.pivot_wider(
+        index=None,
+        names_from=["B"],
+        values_from=["D", "E"],
+        aggfunc=["mean", "sum"],
+        flatten_levels=True,
+    )
+
+    assert_frame_equal(result, expected)
diff --git a/tests/functions/test_select_columns.py b/tests/functions/test_select_columns.py
--- a/tests/functions/test_select_columns.py
+++ b/tests/functions/test_select_columns.py
@@ -114,6 +114,7 @@ def columns(x):
     assert_frame_equal(df, dataframe[expected])
 
 
+@pytest.mark.xfail(reason="Allow tuples which are acceptable in MultiIndex.")
 def test_MultiIndex():
     """
     Raise ValueError if columns is a MultiIndex.
diff --git a/tests/utils/test__select_column.py b/tests/utils/test__select_column.py
--- a/tests/utils/test__select_column.py
+++ b/tests/utils/test__select_column.py
@@ -69,12 +69,14 @@ def test_type(df):
         _select_columns([3, "id"], df)
 
 
+@pytest.mark.xfail(reason="level parameter removed.")
 def test_level_type(df_tuple):
     """Raise TypeError if `level` is the wrong type."""
     with pytest.raises(TypeError):
-        _select_columns("A", df_tuple, level=1.0)
+        _select_columns("A", df_tuple)
 
 
+@pytest.mark.xfail(reason="level parameter removed.")
 def test_level_nonexistent(df_tuple):
     """
     Raise ValueError if column is a MultiIndex
@@ -84,6 +86,7 @@ def test_level_nonexistent(df_tuple):
         _select_columns("A", df_tuple)
 
 
+@pytest.mark.xfail(reason="level parameter removed.")
 def test_tuple_callable(df_tuple):
     """
     Raise ValueError if dataframe has MultiIndex columns
@@ -93,6 +96,7 @@ def test_tuple_callable(df_tuple):
         _select_columns(lambda df: df.name.startswith("A"), df_tuple)
 
 
+@pytest.mark.xfail(reason="level parameter removed.")
 def test_tuple_regex(df_tuple):
     """
     Raise ValueError if dataframe has MultiIndex columns'
@@ -164,6 +168,7 @@ def test_callable(df):
         _select_columns(object, df)
 
 
+@pytest.mark.xfail(reason="Indexing in Pandas is possible with a Series.")
 def test_callable_returns_Series(df):
     """
     Check that error is raised if `columns_to_select` is a
@@ -175,9 +180,8 @@ def test_callable_returns_Series(df):
 
 def test_callable_no_match(df):
     """
-    Raise ValueError if `columns_to_select` is a callable, and
-    no boolean results are returned, when the callable is
-    applied to each series in the dataframe.
+    Raise ValueError if `columns_to_select` is a callable,
+    and no value is returned.
     """
     with pytest.raises(ValueError):
         _select_columns(pd.api.types.is_float_dtype, df)
@@ -325,10 +329,6 @@ def test_tuple(df_tuple):
     """Test _select_columns function on tuple."""
     assert _select_columns(("A", "D"), df_tuple) == ("A", "D")
 
-    assert _select_columns("A", df_tuple, level=0) == ["A"]
-
-    assert _select_columns(re.compile("A"), df_tuple, level=0) == ["A"]
-
 
 def test_list_various(df1):
     """Test _select_columns function on list type."""
@@ -341,7 +341,7 @@ def test_list_various(df1):
         _select_columns(["id", "code*", slice("code", "code2")], df1)
         == df1.filter(regex="^(id|code)").columns.tolist()
     )
-    assert _select_columns([("id", "Name")], df1) == ["id", "Name"]
+    assert _select_columns(["id", "Name"], df1) == ["id", "Name"]
 
 
 def test_list_boolean(df):