Repository: pyjanitor-devs/pyjanitor

Issue:
`pivot_longer` support multiple melting with multiple `values_to`?
# Brief Description

I would like to propose extending `pivot_longer` to support multiple melting with multiple `values_to`.

[Source Example](https://stackoverflow.com/q/51519101/7175713)

# Example API

```python
df = pd.DataFrame({'City': ['Houston', 'Austin', 'Hoover'],
                   'State': ['Texas', 'Texas', 'Alabama'],
                   'Name':['Aria', 'Penelope', 'Niko'],
                   'Mango':[4, 10, 90],
                   'Orange': [10, 8, 14], 
                   'Watermelon':[40, 99, 43],
                   'Gin':[16, 200, 34],
                   'Vodka':[20, 33, 18]},
                 columns=['City', 'State', 'Name', 'Mango', 'Orange', 'Watermelon', 'Gin', 'Vodka'])

df

      City    State      Name  Mango  Orange  Watermelon  Gin  Vodka
0  Houston    Texas      Aria      4      10          40   16     20
1   Austin    Texas  Penelope     10       8          99  200     33
2   Hoover  Alabama      Niko     90      14          43   34     18

df.pivot_longer(index = ['City', 'State'], 
                column_names = slice('Mango', 'Vodka'), 
                names_to = ('Fruit', 'Drinks'), 
                values_to = ('Pounds', 'Ounces'), 
                names_pattern = [r"M|O|W", r"G|V"])

      City    State       Fruit  Pounds  Drink  Ounces
0   Austin    Texas       Mango      10    Gin   200.0
1   Hoover  Alabama       Mango      90    Gin    34.0
2  Houston    Texas       Mango       4    Gin    16.0
3   Austin    Texas      Orange       8  Vodka    33.0
4   Hoover  Alabama      Orange      14  Vodka    18.0
5  Houston    Texas      Orange      10  Vodka    20.0
6   Austin    Texas  Watermelon      99    NaN     NaN
7   Hoover  Alabama  Watermelon      43    NaN     NaN
8  Houston    Texas  Watermelon      40    NaN     NaN
```



Discussion:

Resolving patch:
diff --git a/janitor/functions/pivot.py b/janitor/functions/pivot.py
--- a/janitor/functions/pivot.py
+++ b/janitor/functions/pivot.py
@@ -1,10 +1,10 @@
 import re
-from typing import Callable, List, Optional, Pattern, Tuple, Union
+from typing import Callable, Optional, Pattern, Union
 
 import numpy as np
 import pandas as pd
 import pandas_flavor as pf
-from pandas.api.types import is_list_like, is_re_compilable, is_string_dtype
+from pandas.api.types import is_list_like, is_string_dtype
 
 from janitor.functions.utils import _select_column_names
 from janitor.utils import check
@@ -13,13 +13,13 @@
 @pf.register_dataframe_method
 def pivot_longer(
     df: pd.DataFrame,
-    index: Optional[Union[List, Tuple, str, Pattern]] = None,
-    column_names: Optional[Union[List, Tuple, str, Pattern]] = None,
-    names_to: Optional[Union[List, Tuple, str]] = None,
+    index: Optional[Union[list, tuple, str, Pattern]] = None,
+    column_names: Optional[Union[list, tuple, str, Pattern]] = None,
+    names_to: Optional[Union[list, tuple, str]] = None,
     values_to: Optional[str] = "value",
     column_level: Optional[Union[int, str]] = None,
     names_sep: Optional[Union[str, Pattern]] = None,
-    names_pattern: Optional[Union[List, Tuple, str, Pattern]] = None,
+    names_pattern: Optional[Union[list, tuple, str, Pattern]] = None,
     sort_by_appearance: Optional[bool] = False,
     ignore_index: Optional[bool] = True,
 ) -> pd.DataFrame:
@@ -33,6 +33,7 @@ def pivot_longer(
 
     It is modeled after the `pivot_longer` function in R's tidyr package, and
     offers more functionality and flexibility than `pd.wide_to_long`.
+    It also takes inspiration from R's data.table package.
 
     This function is useful to massage a DataFrame into a format where
     one or more columns are considered measured variables, and all other
@@ -134,7 +135,52 @@ def pivot_longer(
         0    50    1      10      30
         1    50    2      20      40
 
+    Multiple values_to:
 
+        >>> df = pd.DataFrame(
+        ...         {
+        ...             "City": ["Houston", "Austin", "Hoover"],
+        ...             "State": ["Texas", "Texas", "Alabama"],
+        ...             "Name": ["Aria", "Penelope", "Niko"],
+        ...             "Mango": [4, 10, 90],
+        ...             "Orange": [10, 8, 14],
+        ...             "Watermelon": [40, 99, 43],
+        ...             "Gin": [16, 200, 34],
+        ...             "Vodka": [20, 33, 18],
+        ...         },
+        ...         columns=[
+        ...             "City",
+        ...             "State",
+        ...             "Name",
+        ...             "Mango",
+        ...             "Orange",
+        ...             "Watermelon",
+        ...             "Gin",
+        ...             "Vodka",
+        ...         ],
+        ...     )
+        >>> df
+              City    State      Name  Mango  Orange  Watermelon  Gin  Vodka
+        0  Houston    Texas      Aria      4      10          40   16     20
+        1   Austin    Texas  Penelope     10       8          99  200     33
+        2   Hoover  Alabama      Niko     90      14          43   34     18
+        >>> df.pivot_longer(
+        ...         index=["City", "State"],
+        ...         column_names=slice("Mango", "Vodka"),
+        ...         names_to=("Fruit", "Drink"),
+        ...         values_to=("Pounds", "Ounces"),
+        ...         names_pattern=[r"M|O|W", r"G|V"],
+        ...     )
+              City    State       Fruit  Pounds  Drink  Ounces
+        0  Houston    Texas       Mango       4    Gin    16.0
+        1   Austin    Texas       Mango      10    Gin   200.0
+        2   Hoover  Alabama       Mango      90    Gin    34.0
+        3  Houston    Texas      Orange      10  Vodka    20.0
+        4   Austin    Texas      Orange       8  Vodka    33.0
+        5   Hoover  Alabama      Orange      14  Vodka    18.0
+        6  Houston    Texas  Watermelon      40    NaN     NaN
+        7   Austin    Texas  Watermelon      99    NaN     NaN
+        8   Hoover  Alabama  Watermelon      43    NaN     NaN
 
     :param df: A pandas DataFrame.
     :param index: Name(s) of columns to use as identifier variables.
@@ -154,6 +200,8 @@ def pivot_longer(
         from part of the existing column names and overrides`values_to`.
     :param values_to: Name of new column as a string that will contain what
         were previously the values of the columns in `column_names`.
+        values_to can also be a list/tuple
+        and requires that names_pattern is also a list/tuple.
     :param column_level: If columns are a MultiIndex, then use this level to
         unpivot the DataFrame. Provided for compatibility with pandas' melt,
         and applies only if neither `names_sep` nor `names_pattern` is
@@ -275,21 +323,24 @@ def _data_checks_pivot_longer(
             names_to = [*names_to]
         check("names_to", names_to, [list])
 
-        unique_names_to = set()
+        uniques = set()
         for word in names_to:
-            if not isinstance(word, str):
-                raise TypeError(
-                    "All entries in the names_to argument must be strings. "
-                    f"{word} is of type {type(word).__name__}."
-                )
-
-            if (word in unique_names_to) and (word != ".value"):
-                raise ValueError(f"{word} already exists in names_to.")
-            unique_names_to.add(word)  # noqa: PD005
-        unique_names_to = None
+            check(f"{word} in names_to", word, [str])
+            if (word in uniques) and (word != ".value"):
+                raise ValueError(f"{word} is duplicated in names_to.")
+            uniques.add(word)
 
         len_names_to = len(names_to)
 
+    check("values_to", values_to, [str, list, tuple])
+    if isinstance(values_to, (list, tuple)) and (
+        not isinstance(names_pattern, (list, tuple))
+    ):
+        raise TypeError(
+            "values_to can be a list/tuple only "
+            "if names_pattern is a list/tuple."
+        )
+
     if names_sep and names_pattern:
         raise ValueError(
             "Only one of names_pattern or names_sep should be provided."
@@ -311,15 +362,9 @@ def _data_checks_pivot_longer(
                     f"is {num_regex_grps}."
                 )
 
-        if isinstance(names_pattern, (list, tuple)):
+        elif isinstance(names_pattern, (list, tuple)):
             for word in names_pattern:
-                if not is_re_compilable(word):
-                    raise TypeError(
-                        f"All entries in the names_pattern argument "
-                        "must be regular expressions, "
-                        "or can be compiled into a regex. "
-                        f"`{word}` is of type {type(word).__name__}."
-                    )
+                check(f"{word} in names_pattern", word, [str, Pattern])
 
             if len(names_pattern) != len_names_to:
                 raise ValueError(
@@ -335,18 +380,37 @@ def _data_checks_pivot_longer(
                     "if names_pattern is a list/tuple."
                 )
 
+            if isinstance(values_to, (list, tuple)):
+                if len(values_to) != len(names_pattern):
+                    raise ValueError(
+                        f"The length of values_to does not match "
+                        "the number of regexes in names_pattern. "
+                        f"The length of values_to is {len(values_to)} "
+                        f"while the number of regexes is {len(names_pattern)}."
+                    )
+                uniques = set()
+                for word in values_to:
+                    check(f"{word} in values_to", word, [str])
+                    if word in names_to:
+                        raise ValueError(
+                            f"{word} in values_to already exists in names_to."
+                        )
+
+                    if word in uniques:
+                        raise ValueError(f"{word} is duplicated in values_to.")
+                    uniques.add(word)
+
     if names_sep is not None:
         check("names_sep", names_sep, [str, Pattern])
         if names_to is None:
             raise ValueError("Kindly provide values for names_to.")
 
-    check("values_to", values_to, [str])
     df_columns = df.columns
 
     dot_value = (names_to is not None) and (
         (".value" in names_to) or (isinstance(names_pattern, (list, tuple)))
     )
-    if (values_to in df_columns) and (not dot_value):
+    if (not dot_value) and (values_to in df_columns):
         # copied from pandas' melt source code
         # with a minor tweak
         raise ValueError(
@@ -474,23 +538,21 @@ def _computations_pivot_longer(
     if column_names:
         df = df.loc[:, column_names]
 
-    # df_index_names = df.index.names
-
     # check to avoid duplicate columns in the final dataframe
     # idea is that if there is no `.value`
     # then the word should not exist in the index
     # there is no need to check the columns
     # since that is what we are replacing with names_to
-    if ".value" not in names_to:
-        for word in names_to:
-            if index and (word in index):
-                raise ValueError(
-                    f"`{word}` in names_to already exists "
-                    "as a column label assigned to the dataframe's index. "
-                    "Kindly use a unique name."
-                )
+    if (".value" not in names_to) and index:
+        exclude = set(names_to).intersection(index)
+        if exclude:
+            raise ValueError(
+                f"Labels {*exclude, } in names_to already exist "
+                "as column labels assigned to the dataframe's "
+                "index parameter. Kindly provide unique label(s)."
+            )
 
-    if names_sep:
+    if names_sep is not None:
         return _pivot_longer_names_sep(
             df,
             index,
@@ -513,7 +575,13 @@ def _computations_pivot_longer(
         )
 
     return _pivot_longer_names_pattern_sequence(
-        df, index, names_to, names_pattern, sort_by_appearance, ignore_index
+        df,
+        index,
+        names_to,
+        names_pattern,
+        sort_by_appearance,
+        values_to,
+        ignore_index,
     )
 
 
@@ -585,7 +653,7 @@ def _sort_by_appearance_for_melt(
 
 def _pivot_longer_not_dot_value(
     df: pd.DataFrame,
-    index,
+    index: Union[list, None],
     sort_by_appearance: bool,
     ignore_index: bool,
     values_to: str,
@@ -606,7 +674,7 @@ def _pivot_longer_not_dot_value(
         df = _sort_by_appearance_for_melt(df=df, len_index=len_index)
 
     if index:
-        df = df.reset_index(index)
+        df = df.reset_index(level=index)
 
     if ignore_index:
         df.index = range(len(df))
@@ -639,21 +707,25 @@ def _pivot_longer_dot_value(
     # check to avoid duplicate columns
     # the names assigned to `.value` should not be found in the index
     # and should not be found in the other names in names_to
-    exclude = mapping[".value"].array
-    for word in names_to:
-        if (word != ".value") and (word in exclude):
-            raise ValueError(
-                f"{word} in names_to already exists "
-                "in the new dataframe's columns. "
-                "Kindly use a unique name."
-            )
+    exclude = [
+        word
+        for word in mapping[".value"].array
+        if (word in names_to) and (word != ".value")
+    ]
+    if exclude:
+        raise ValueError(
+            f"Labels {*exclude, } in names_to already exist "
+            "in the new dataframe's columns. "
+            "Kindly provide unique label(s)."
+        )
+
     if index:
-        exclude = set(index).intersection(exclude)
+        exclude = set(index).intersection(mapping[".value"])
         if exclude:
             raise ValueError(
                 f"Labels {*exclude, } already exist "
                 "as column labels assigned to the dataframe's "
-                "index parameter. Kindly use a unique name."
+                "index parameter. Kindly provide unique label(s)."
             )
     # reorder allows for easy column selection later
     # in the concatenation phase
@@ -685,9 +757,9 @@ def _pivot_longer_dot_value(
         else:
             mapping = pd.MultiIndex.from_frame(mapping)
     else:
-        mapping = [mapping.groupby(names_to, sort=False).cumcount()] + [
-            series for _, series in mapping.items()
-        ]
+        mapping = [
+            mapping.groupby(names_to, sort=False, observed=True).cumcount()
+        ] + [series for _, series in mapping.items()]
         mapping = pd.MultiIndex.from_arrays(mapping)
     df.columns = mapping
 
@@ -704,7 +776,7 @@ def _pivot_longer_dot_value(
         df = pd.concat(df, keys=others, axis="index", copy=False, sort=False)
         # intercept and reorder column if needed
         if not df.columns.equals(_value):
-            df = df.reindex(columns=_value)
+            df = df.loc[:, _value]
     if index:
         num_levels = df.index.nlevels
         # need to order the dataframe's index
@@ -745,13 +817,22 @@ def _pivot_longer_names_pattern_sequence(
     names_to: list,
     names_pattern: Union[list, tuple],
     sort_by_appearance: bool,
+    values_to: Union[str, list, tuple],
     ignore_index: bool,
 ) -> pd.DataFrame:
     """
     This takes care of pivoting scenarios where
     names_pattern is provided, and is a list/tuple.
     """
-
+    values_to_is_a_sequence = isinstance(values_to, (list, tuple))
+    if values_to_is_a_sequence and index:
+        exclude = [word for word in values_to if word in index]
+        if exclude:
+            raise ValueError(
+                f"Labels {*exclude, } in values_to already exist as "
+                "column labels assigned to the dataframe's index parameter. "
+                "Kindly use unique labels."
+            )
     df_columns = df.columns
     len_index = len(df)
 
@@ -770,14 +851,23 @@ def _pivot_longer_names_pattern_sequence(
                 f"at position {position} -> {names_pattern[position]}."
             )
 
-    mapping = np.select(mapping, names_to, None)
+    if values_to_is_a_sequence:
+        mapping, other = np.select(mapping, values_to, None), np.select(
+            mapping, names_to, None
+        )
+    else:
+        mapping = np.select(mapping, names_to, None)
 
     # only matched columns are retained
     matches = pd.notna(mapping)
 
     df = df.loc[:, matches]
     mapping = mapping[matches]
-    if len(mapping) == 1:
+    if values_to_is_a_sequence:
+        other = other[matches]
+        matches = df.columns
+
+    if isinstance(values_to, str) and (len(mapping) == 1):
         df.columns = mapping
     else:
         mapping = pd.Series(mapping)
@@ -796,16 +886,43 @@ def _pivot_longer_names_pattern_sequence(
         #    2   3   7   6   9  12
         # then x2 will pair with y1 and x1 will pair with y2
         # it is simply a first come first serve approach
-        positions = mapping.groupby(mapping, sort=False).cumcount()
+        # the same idea applies if values_to is a list/tuple
+        positions = mapping.groupby(
+            mapping, sort=False, observed=True
+        ).cumcount()
+        # positions ensure uniqueness, and we take advantage of this
+        # in creating sub dataframes that are then combined back into
+        # one
         df.columns = [positions, mapping]
         df = [df.loc[:, num] for num in positions.unique()]
         df = pd.concat(df, axis="index", sort=False, copy=False)
+        if values_to_is_a_sequence:
+            # chuck the index aside, add the columns
+            # then return the index.
+            # This is necessary to avoid Pandas' ValueError
+            # which occurs due to incompatible lengths.
+            indexer = df.index
+            df.index = range(len(df))
+            # get the grouping of names_to to the columns
+            # the values in names_to become column names
+            # while the associated column labels in df
+            # become values in the new columns, repeated
+            # as many times as the length of the original df
+            for key, value in matches.groupby(other).items():
+                value = value.repeat(len_index)
+                df[key] = pd.Series(value)
+            df.index = indexer
+            names_to = [
+                label for arg in zip(names_to, values_to) for label in arg
+            ]
+            df = df.loc[:, names_to]
+            indexer = None
 
     if sort_by_appearance:
         df = _sort_by_appearance_for_melt(df=df, len_index=len_index)
 
     if index:
-        df = df.reset_index(index)
+        df = df.reset_index(level=index)
 
     if ignore_index:
         df.index = range(len(df))
@@ -857,7 +974,6 @@ def _pivot_longer_names_pattern_str(
         )
 
     # .value
-
     return _pivot_longer_dot_value(
         df, index, sort_by_appearance, ignore_index, names_to, mapping
     )
@@ -893,7 +1009,10 @@ def _pivot_longer_names_sep(
     mapping.columns = names_to
 
     if ".value" not in names_to:
-        df.columns = pd.MultiIndex.from_frame(mapping)
+        if len(names_to) == 1:
+            df.columns = mapping.iloc[:, 0]
+        else:
+            df.columns = pd.MultiIndex.from_frame(mapping)
         return _pivot_longer_not_dot_value(
             df, index, sort_by_appearance, ignore_index, values_to
         )
@@ -907,9 +1026,9 @@ def _pivot_longer_names_sep(
 @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,
+    index: Optional[Union[list, str]] = None,
+    names_from: Optional[Union[list, str]] = None,
+    values_from: Optional[Union[list, str]] = None,
     levels_order: Optional[list] = None,
     flatten_levels: Optional[bool] = True,
     names_sep="_",
@@ -1044,9 +1163,9 @@ def pivot_wider(
 
 def _computations_pivot_wider(
     df: pd.DataFrame,
-    index: Optional[Union[List, str]] = None,
-    names_from: Optional[Union[List, str]] = None,
-    values_from: Optional[Union[List, str]] = None,
+    index: Optional[Union[list, str]] = None,
+    names_from: Optional[Union[list, str]] = None,
+    values_from: Optional[Union[list, str]] = None,
     levels_order: Optional[list] = None,
     flatten_levels: Optional[bool] = True,
     names_sep="_",


Test patch:
diff --git a/tests/functions/test_conditional_join.py b/tests/functions/test_conditional_join.py
--- a/tests/functions/test_conditional_join.py
+++ b/tests/functions/test_conditional_join.py
@@ -302,18 +302,17 @@ def test_single_condition_less_than_ints_extension_array(df, right):
 
     df = df.assign(A=df["A"].astype("Int64"))
     right = right.assign(Integers=right["Integers"].astype(pd.Int64Dtype()))
-    left_on, right_on = ["A", "Integers"]
+
     expected = (
         df.merge(right, how="cross")
-        .query(f"{left_on} < {right_on}")
+        .loc[lambda df: df.A < df.Integers, ["A", "Integers"]]
         .reset_index(drop=True)
     )
-    expected = expected.filter([left_on, right_on])
+
     actual = df.conditional_join(
-        right, (left_on, right_on, "<"), how="inner", sort_by_appearance=True
-    )
+        right, ("A", "Integers", "<"), how="inner", sort_by_appearance=True
+    ).filter(["A", "Integers"])
 
-    actual = actual.filter([left_on, right_on])
     assert_frame_equal(expected, actual)
 
 
@@ -427,15 +426,14 @@ def test_single_condition_greater_than_ints_extension_array(df, right):
     right = right.astype({"Integers": "Int64"})
     expected = (
         df.merge(right, how="cross")
-        .query(f"{left_on} > {right_on}")
+        .loc[lambda df: df.A > df.Integers, ["A", "Integers"]]
         .reset_index(drop=True)
     )
-    expected = expected.filter([left_on, right_on])
+
     actual = df.conditional_join(
         right, (left_on, right_on, ">"), how="inner", sort_by_appearance=True
-    )
+    ).filter(["A", "Integers"])
 
-    actual = actual.filter([left_on, right_on])
     assert_frame_equal(expected, actual)
 
 
@@ -808,21 +806,19 @@ def test_dual_ne_extension(df, right):
     filters = ["A", "Integers", "B", "Numeric"]
     df = df.astype({"A": "Int64"})
     right = right.astype({"Integers": "Int64"})
-    expected = (
-        df.merge(right, how="cross")
-        .query("A != Integers and B != Numeric")
-        .reset_index(drop=True)
-    )
-    expected = expected.filter(filters)
+    expected = df.merge(right, how="cross")
+    expected = expected.loc[
+        expected.A.ne(expected.Integers) & expected.B.ne(expected.Numeric),
+        filters,
+    ].reset_index(drop=True)
+
     actual = df.conditional_join(
         right,
         ("A", "Integers", "!="),
         ("B", "Numeric", "!="),
         how="inner",
         sort_by_appearance=True,
-    )
-
-    actual = actual.filter(filters)
+    ).filter(filters)
     assert_frame_equal(expected, actual)
 
 
@@ -835,12 +831,12 @@ def test_dual_ne(df, right):
 
     filters = ["A", "Integers", "B", "Numeric"]
 
-    expected = (
-        df.merge(right, how="cross")
-        .query("A != Integers and B != Numeric")
-        .reset_index(drop=True)
-    )
-    expected = expected.filter(filters)
+    expected = df.merge(right, how="cross")
+    expected = expected.loc[
+        expected.A.ne(expected.Integers) & expected.B.ne(expected.Numeric),
+        filters,
+    ].reset_index(drop=True)
+
     actual = df.conditional_join(
         right,
         ("A", "Integers", "!="),
@@ -1071,12 +1067,14 @@ def test_ge_le_ne_extension_array(df, right):
     df = df.assign(A=df["A"].astype("Int64"))
     right = right.assign(Integers=right["Integers"].astype(pd.Int64Dtype()))
 
-    expected = (
-        df.merge(right, how="cross")
-        .query("A != Integers and B < Numeric and E >= Dates")
-        .reset_index(drop=True)
-    )
-    expected = expected.filter(filters)
+    expected = df.merge(right, how="cross")
+    expected = expected.loc[
+        expected.A.ne(expected.Integers)
+        & expected.B.lt(expected.Numeric)
+        & expected.E.ge(expected.Dates),
+        filters,
+    ].reset_index(drop=True)
+
     actual = df.conditional_join(
         right,
         ("E", "Dates", ">="),
@@ -1084,9 +1082,8 @@ def test_ge_le_ne_extension_array(df, right):
         ("B", "Numeric", "<"),
         how="inner",
         sort_by_appearance=True,
-    )
+    ).filter(filters)
 
-    actual = actual.filter(filters)
     assert_frame_equal(expected, actual)
 
 
@@ -1101,14 +1098,15 @@ def test_ge_lt_ne_extension(df, right):
     df = df.assign(A=df["A"].astype("Int64"))
     right = right.assign(Integers=right["Integers"].astype(pd.Int64Dtype()))
 
-    expected = (
-        df.merge(right, how="cross")
-        .query(
-            "A < Integers and B != Numeric and E >= Dates and E != Dates_Right"
-        )
-        .reset_index(drop=True)
-    )
-    expected = expected.filter(filters)
+    expected = df.merge(right, how="cross")
+    expected = expected.loc[
+        expected.A.lt(expected.Integers)
+        & expected.B.ne(expected.Numeric)
+        & expected.E.ge(expected.Dates)
+        & expected.E.ne(expected.Dates_Right),
+        filters,
+    ].reset_index(drop=True)
+
     actual = df.conditional_join(
         right,
         ("E", "Dates", ">="),
@@ -1192,18 +1190,15 @@ def test_ge_lt_ne_extension_variant(df, right):
     df = df.assign(A=df["A"].astype("Int64"))
     right = right.assign(Integers=right["Integers"].astype(pd.Int64Dtype()))
 
-    expected = (
-        df.assign(t=1)
-        .merge(
-            right.assign(t=1),
-            on="t",
-        )
-        .query(
-            "A != Integers and B < Numeric and E >= Dates and E != Dates_Right"
-        )
-        .reset_index(drop=True)
-    )
-    expected = expected.filter(filters)
+    expected = df.merge(right, how="cross")
+    expected = expected.loc[
+        expected.A.ne(expected.Integers)
+        & expected.B.lt(expected.Numeric)
+        & expected.E.ge(expected.Dates)
+        & expected.E.ne(expected.Dates_Right),
+        filters,
+    ].reset_index(drop=True)
+
     actual = df.conditional_join(
         right,
         ("E", "Dates", ">="),
@@ -1212,9 +1207,7 @@ def test_ge_lt_ne_extension_variant(df, right):
         ("E", "Dates_Right", "!="),
         how="inner",
         sort_by_appearance=True,
-    )
-
-    actual = actual.filter(filters)
+    ).filter(filters)
     assert_frame_equal(expected, actual)
 
 
@@ -1502,18 +1495,17 @@ def test_multiple_eqs_extension_array(df, right):
     columns = ["B", "A", "E", "Floats", "Integers", "Dates"]
     df = df.assign(A=df["A"].astype("Int64"))
     right = right.assign(Integers=right["Integers"].astype(pd.Int64Dtype()))
-    expected = (
-        df.merge(
-            right,
-            left_on=["B", "E"],
-            right_on=["Floats", "Dates"],
-            how="inner",
-            sort=False,
-        )
-        # .dropna(subset=["B", "E", "Floats", "Dates"])
-        .query("A != Integers").reset_index(drop=True)
+    expected = df.merge(
+        right,
+        left_on=["B", "E"],
+        right_on=["Floats", "Dates"],
+        how="inner",
+        sort=False,
     )
-    expected = expected.filter(columns)
+    expected = expected.loc[
+        expected.A != expected.Integers, columns
+    ].reset_index(drop=True)
+
     actual = df.conditional_join(
         right,
         ("E", "Dates", "=="),
@@ -1521,7 +1513,5 @@ def test_multiple_eqs_extension_array(df, right):
         ("A", "Integers", "!="),
         how="inner",
         sort_by_appearance=False,
-    )
-
-    actual = actual.filter(columns)
+    ).filter(columns)
     assert_frame_equal(expected, actual)
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
@@ -60,13 +60,13 @@ def test_subtype_names_to(df_checks):
     and the wrong type is provided for entries
     in names_to.
     """
-    with pytest.raises(TypeError):
-        df_checks.pivot_longer(names_to=[("famid",)])
+    with pytest.raises(TypeError, match="1 in names_to.+"):
+        df_checks.pivot_longer(names_to=[1])
 
 
 def test_duplicate_names_to(df_checks):
     """Raise error if names_to contains duplicates."""
-    with pytest.raises(ValueError, match="y already exists in names_to."):
+    with pytest.raises(ValueError, match="y is duplicated in names_to."):
         df_checks.pivot_longer(names_to=["y", "y"], names_pattern="(.+)(.)")
 
 
@@ -75,7 +75,10 @@ def test_both_names_sep_and_pattern(df_checks):
     Raise ValueError if both names_sep
     and names_pattern is provided.
     """
-    with pytest.raises(ValueError):
+    with pytest.raises(
+        ValueError,
+        match="Only one of names_pattern or names_sep should be provided.",
+    ):
         df_checks.pivot_longer(
             names_to=["rar", "bar"], names_sep="-", names_pattern="(.+)(.)"
         )
@@ -83,7 +86,7 @@ def test_both_names_sep_and_pattern(df_checks):
 
 def test_name_pattern_wrong_type(df_checks):
     """Raise TypeError if the wrong type provided for names_pattern."""
-    with pytest.raises(TypeError):
+    with pytest.raises(TypeError, match="names_pattern should be one of.+"):
         df_checks.pivot_longer(names_to=["rar", "bar"], names_pattern=2007)
 
 
@@ -99,7 +102,11 @@ def test_name_pattern_groups_len(df_checks):
     and the number of groups
     differs from the length of names_to.
     """
-    with pytest.raises(ValueError):
+    with pytest.raises(
+        ValueError,
+        match="The length of names_to does not match "
+        "the number of groups in names_pattern.+",
+    ):
         df_checks.pivot_longer(names_to=".value", names_pattern="(.+)(.)")
 
 
@@ -108,7 +115,7 @@ def test_names_pattern_wrong_subtype(df_checks):
     Raise TypeError if names_pattern is a list/tuple
     and wrong subtype is supplied.
     """
-    with pytest.raises(TypeError):
+    with pytest.raises(TypeError, match="1 in names_pattern.+"):
         df_checks.pivot_longer(
             names_to=["ht", "num"], names_pattern=[1, "\\d"]
         )
@@ -119,7 +126,11 @@ def test_names_pattern_names_to_unequal_length(df_checks):
     Raise ValueError if names_pattern is a list/tuple
     and wrong number of items in names_to.
     """
-    with pytest.raises(ValueError):
+    with pytest.raises(
+        ValueError,
+        match="The length of names_to does not match "
+        "the number of regexes in names_pattern.+",
+    ):
         df_checks.pivot_longer(
             names_to=["variable"], names_pattern=["^ht", ".+i.+"]
         )
@@ -130,7 +141,11 @@ def test_names_pattern_names_to_dot_value(df_checks):
     Raise Error if names_pattern is a list/tuple and
     .value in names_to.
     """
-    with pytest.raises(ValueError):
+    with pytest.raises(
+        ValueError,
+        match=".value is not accepted in names_to "
+        "if names_pattern is a list/tuple.",
+    ):
         df_checks.pivot_longer(
             names_to=["variable", ".value"], names_pattern=["^ht", ".+i.+"]
         )
@@ -138,7 +153,7 @@ def test_names_pattern_names_to_dot_value(df_checks):
 
 def test_name_sep_wrong_type(df_checks):
     """Raise TypeError if the wrong type is provided for names_sep."""
-    with pytest.raises(TypeError):
+    with pytest.raises(TypeError, match="names_sep should be one of.+"):
         df_checks.pivot_longer(names_to=[".value", "num"], names_sep=["_"])
 
 
@@ -150,10 +165,74 @@ def test_name_sep_no_names_to(df_checks):
 
 def test_values_to_wrong_type(df_checks):
     """Raise TypeError if the wrong type is provided for `values_to`."""
-    with pytest.raises(TypeError):
+    with pytest.raises(TypeError, match="values_to should be one of.+"):
+        df_checks.pivot_longer(values_to={"salvo"})
+
+
+def test_values_to_wrong_type_names_pattern(df_checks):
+    """
+    Raise TypeError if `values_to` is a list,
+    and names_pattern is not.
+    """
+    with pytest.raises(
+        TypeError,
+        match="values_to can be a list/tuple only "
+        "if names_pattern is a list/tuple.",
+    ):
         df_checks.pivot_longer(values_to=["salvo"])
 
 
+def test_values_to_names_pattern_unequal_length(df_checks):
+    """
+    Raise ValueError if `values_to` is a list,
+    and the length of names_pattern
+    does not match the length of values_to.
+    """
+    with pytest.raises(
+        ValueError,
+        match="The length of values_to does not match "
+        "the number of regexes in names_pattern.+",
+    ):
+        df_checks.pivot_longer(
+            values_to=["salvo"],
+            names_pattern=["ht", r"\d"],
+            names_to=["foo", "bar"],
+        )
+
+
+def test_values_to_names_seq_names_to(df_checks):
+    """
+    Raise ValueError if `values_to` is a list,
+    and intersects with names_to.
+    """
+    with pytest.raises(
+        ValueError, match="salvo in values_to already exists in names_to."
+    ):
+        df_checks.pivot_longer(
+            values_to=["salvo"], names_pattern=["ht"], names_to="salvo"
+        )
+
+
+def test_sub_values_to(df_checks):
+    """Raise error if values_to is a sequence, and contains non strings."""
+    with pytest.raises(TypeError, match="1 in values_to.+"):
+        df_checks.pivot_longer(
+            names_to=["x", "y"],
+            names_pattern=[r"ht", r"\d"],
+            values_to=[1, "salvo"],
+        )
+
+
+def test_duplicate_values_to(df_checks):
+    """Raise error if values_to is a sequence, and contains duplicates."""
+    with pytest.raises(ValueError, match="salvo is duplicated in values_to."):
+        df_checks.pivot_longer(
+            names_to=["x", "y"],
+            names_pattern=[r"ht", r"\d"],
+            values_to=["salvo", "salvo"],
+        )
+
+
 def test_values_to_exists_in_columns(df_checks):
     """
     Raise ValueError if values_to already
@@ -266,19 +345,43 @@ def test_dot_value_names_to_columns_intersect(df_checks):
     .value in names_to,
     and names_to intersects with the new columns
     """
-    with pytest.raises(ValueError):
+    with pytest.raises(
+        ValueError,
+        match=r".+in names_to already exist in the new dataframe\'s columns.+",
+    ):
         df_checks.pivot_longer(
             index="famid", names_to=(".value", "ht"), names_pattern="(.+)(.)"
         )
 
 
+def test_values_to_seq_index_intersect(df_checks):
+    """
+    Raise ValueError if values_to is a sequence,
+    and intersects with the index
+    """
+    match = ".+values_to already exist as column labels assigned "
+    match = match + "to the dataframe's index parameter.+"
+    with pytest.raises(ValueError, match=rf"{match}"):
+        df_checks.pivot_longer(
+            index="famid",
+            names_to=("value", "ht"),
+            names_pattern=["ht", r"\d"],
+            values_to=("famid", "foo"),
+        )
+
+
 def test_dot_value_names_to_index_intersect(df_checks):
     """
     Raise ValueError if names_sep/names_pattern,
     .value in names_to,
     and names_to intersects with the index
     """
-    with pytest.raises(ValueError):
+    match = ".+already exist as column labels assigned "
+    match = match + "to the dataframe's index parameter.+"
+    with pytest.raises(
+        ValueError,
+        match=rf"{match}",
+    ):
         df_checks.rename(columns={"famid": "ht"}).pivot_longer(
             index="ht", names_to=(".value", "num"), names_pattern="(.+)(.)"
         )
@@ -289,7 +392,9 @@ def test_names_pattern_list_empty_any(df_checks):
     Raise ValueError if names_pattern is a list,
     and not all matches are returned.
     """
-    with pytest.raises(ValueError):
+    with pytest.raises(
+        ValueError, match="No match was returned for the regex.+"
+    ):
         df_checks.pivot_longer(
             index=["famid", "birth"],
             names_to=["ht"],
@@ -480,7 +585,7 @@ def test_names_pattern_str(test_df):
             sep="_",
             i="index",
             j="set",
-            suffix=".+",
+            suffix=r".+",
         )
         .reset_index("set")
         .reset_index(drop=True)
@@ -589,6 +694,23 @@ def test_not_dot_value_sep(not_dot_value):
     assert_frame_equal(result, actual)
 
 
+def test_not_dot_value_sep2(not_dot_value):
+    """Test output when names_sep and no dot_value"""
+
+    result = not_dot_value.pivot_longer(
+        "country",
+        names_to="event",
+        names_sep="/",
+        values_to="score",
+    )
+
+    actual = not_dot_value.melt(
+        "country", var_name="event", value_name="score"
+    )
+
+    assert_frame_equal(result, actual)
+
+
 def test_not_dot_value_pattern(not_dot_value):
     """Test output when names_pattern is a string and no dot_value"""
 
@@ -764,7 +886,7 @@ def test_names_pattern_seq_single_column(single_val):
     Test output if names_pattern is a list.
     """
     df = single_val[["id", "x1"]]
-    result = df.pivot_longer(index="id", names_to="yA", names_pattern=["(.+)"])
+    result = df.pivot_longer(index="id", names_to="yA", names_pattern=[".+"])
 
     assert_frame_equal(result, df.rename(columns={"x1": "yA"}))
 
@@ -810,3 +932,93 @@ def test_names_pattern_nulls_in_data():
     )
 
     assert_frame_equal(result, actual)
+
+
+@pytest.fixture
+def multiple_values_to():
+    """fixture for multiple values_to"""
+    # https://stackoverflow.com/q/51519101/7175713
+    return pd.DataFrame(
+        {
+            "City": ["Houston", "Austin", "Hoover"],
+            "State": ["Texas", "Texas", "Alabama"],
+            "Name": ["Aria", "Penelope", "Niko"],
+            "Mango": [4, 10, 90],
+            "Orange": [10, 8, 14],
+            "Watermelon": [40, 99, 43],
+            "Gin": [16, 200, 34],
+            "Vodka": [20, 33, 18],
+        },
+        columns=[
+            "City",
+            "State",
+            "Name",
+            "Mango",
+            "Orange",
+            "Watermelon",
+            "Gin",
+            "Vodka",
+        ],
+    )
+
+
+def test_output_values_to_seq(multiple_values_to):
+    """Test output when values_to is a list/tuple."""
+
+    actual = multiple_values_to.melt(
+        ["City", "State"],
+        value_vars=["Mango", "Orange", "Watermelon"],
+        var_name="Fruit",
+        value_name="Pounds",
+    )
+
+    expected = multiple_values_to.pivot_longer(
+        index=["City", "State"],
+        column_names=slice("Mango", "Watermelon"),
+        names_to=("Fruit"),
+        values_to=("Pounds",),
+        names_pattern=[r"M|O|W"],
+    )
+
+    assert_frame_equal(expected, actual)
+
+
+def test_output_values_to_seq1(multiple_values_to):
+    """Test output when values_to is a list/tuple."""
+    # https://stackoverflow.com/a/51520155/7175713
+    df1 = multiple_values_to.melt(
+        id_vars=["City", "State"],
+        value_vars=["Mango", "Orange", "Watermelon"],
+        var_name="Fruit",
+        value_name="Pounds",
+    )
+    df2 = multiple_values_to.melt(
+        id_vars=["City", "State"],
+        value_vars=["Gin", "Vodka"],
+        var_name="Drink",
+        value_name="Ounces",
+    )
+
+    df1 = df1.set_index(
+        ["City", "State", df1.groupby(["City", "State"]).cumcount()]
+    )
+    df2 = df2.set_index(
+        ["City", "State", df2.groupby(["City", "State"]).cumcount()]
+    )
+
+    actual = (
+        pd.concat([df1, df2], axis=1)
+        .sort_index(level=2)
+        .reset_index(level=2, drop=True)
+        .reset_index()
+    )
+
+    expected = multiple_values_to.pivot_longer(
+        index=["City", "State"],
+        column_names=slice("Mango", "Vodka"),
+        names_to=("Fruit", "Drink"),
+        values_to=("Pounds", "Ounces"),
+        names_pattern=[r"M|O|W", r"G|V"],
+    ).sort_values(["Fruit", "City", "State"], ignore_index=True)
+
+    assert_frame_equal(expected, actual)