Repository: pyjanitor-devs/pyjanitor (python)

Issue:
KeyError from pivot_longer
# Brief Description
When I ran pivot_longer without giving column_names, I got a KeyError, which was not helpful, nor documented on the pivot_longer man page, https://pyjanitor.readthedocs.io/reference/janitor.functions/janitor.pivot_longer.html#janitor.pivot_longer
I expected a more informative error message, or at least for this error to be documented.

# System Information


- Operating system: /Linux/  <!-- delete the appropriate ones -->
- OS details (optional):  ubuntu 20.4
- Python version (required): 3.7.6

# Minimally Reproducible Code
```
import pandas as pd
import janitor
d=pd.DataFrame({"Petal.Width":[1], "Sepal.Width":[2], "Petal.Length":[3], "Species":"setosa", "Sepal.Length":[4]})
d.pivot_longer(column_names = ["Petal.Width", "Sepal.Width","Petal.Length","Sepal.Length"], names_pattern="(.*)[.](.*)", names_to=["part", "dim"])
d.pivot_longer(names_pattern="(.*)[.](.*)", names_to=["part", "dim"])
```

# Error Messages

output I got:
```
Python 3.7.6 (default, Jan  8 2020, 19:59:22) 
[GCC 7.3.0] :: Anaconda, Inc. on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> python.el: native completion setup loaded
>>> import pandas as pd
>>> import janitor
>>> d=pd.DataFrame({"Petal.Width":[1], "Sepal.Width":[2], "Petal.Length":[3], "Species":"setosa", "Sepal.Length":[4]})
>>> d.pivot_longer(column_names = ["Petal.Width", "Sepal.Width","Petal.Length","Sepal.Length"], names_pattern="(.*)[.](.*)", names_to=["part", "dim"])
  Species   part     dim  value
0  setosa  Petal   Width      1
1  setosa  Sepal   Width      2
2  setosa  Petal  Length      3
3  setosa  Sepal  Length      4
>>> d.pivot_longer(names_pattern="(.*)[.](.*)", names_to=["part", "dim"])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/tdhock/teaching/regex-tutorial/janitor_bug.py", line 5, in <module>
    d.pivot_longer(names_pattern="(.*)[.](.*)", names_to=["part", "dim"])
  File "/home/tdhock/.local/share/r-miniconda/lib/python3.7/site-packages/pandas_flavor/register.py", line 29, in __call__
    return method(self._obj, *args, **kwargs)
  File "/home/tdhock/.local/share/r-miniconda/lib/python3.7/site-packages/janitor/functions.py", line 5775, in pivot_longer
    ignore_index,
  File "/home/tdhock/.local/share/r-miniconda/lib/python3.7/site-packages/janitor/utils.py", line 1475, in _computations_pivot_longer
    names_pattern=names_pattern,
  File "/home/tdhock/.local/share/r-miniconda/lib/python3.7/site-packages/janitor/utils.py", line 1351, in _pivot_longer_extractions
    mapping.loc[:, group] = mapping.groupby(".value").cumcount()
  File "/home/tdhock/.local/share/r-miniconda/lib/python3.7/site-packages/pandas/core/frame.py", line 6525, in groupby
    dropna=dropna,
  File "/home/tdhock/.local/share/r-miniconda/lib/python3.7/site-packages/pandas/core/groupby/groupby.py", line 533, in __init__
    dropna=self.dropna,
  File "/home/tdhock/.local/share/r-miniconda/lib/python3.7/site-packages/pandas/core/groupby/grouper.py", line 786, in get_grouper
    raise KeyError(gpr)
KeyError: '.value'
>>> 
```


Pull request description:
[ENH] Pivot longer and other fixes
# PR Description

Please describe the changes proposed in the pull request:

- Clearer error messages for `pivot_longer` h/t @tdhock 
- Refactored `encode_categorical` to use functools.dispatch
- `select_columns` uses variable arguments (*args), allowing for a cleaner selection, without the need for lists for multiple selections.

<!-- Doing so provides maintainers with context on what the PR is, and can help us more effectively review your PR. -->

<!-- Please also identify below which issue that has been raised that you are going to close. -->

**This PR resolves #836.**

<!-- As you go down the PR template, please feel free to delete sections that are irrelevant. -->

# PR Checklist

<!-- This checklist exists for newcomers who are not yet familiar with our requirements. If you are experienced with
the project, please feel free to delete this section. -->

Please ensure that you have done the following:

1. [x] PR in from a fork off your branch. Do not PR from `<your_username>`:`dev`, but rather from `<your_username>`:`<feature-branch_name>`.
<!-- Doing this helps us keep the commit history much cleaner than it would otherwise be. -->
2. [x] If you're not on the contributors list, add yourself to `AUTHORS.rst`.
<!-- We'd like to acknowledge your contributions! -->
3. [x] Add a line to `CHANGELOG.md` under the latest version header (i.e. the one that is "on deck") describing the contribution.
    - Do use some discretion here; if there are multiple PRs that are related, keep them in a single line.

# Automatic checks

There will be automatic checks run on the PR. These include:

- Building a preview of the docs on Netlify
- Automatically linting the code
- Making sure the code is documented
- Making sure that all tests are passed
- Making sure that code coverage doesn't go down.

# Relevant Reviewers

<!-- Finally, please tag relevant maintainers to review. -->

Please tag maintainers to review.

- @ericmjl


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a18b57a..0d6cc0a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,12 @@
 -   [ENH] Change `coalesce` to return columns; also use `bfill`, `ffill`,
     which is faster than `combine_first` @samukweku
 -   [ENH] Use `eval` for string conditions in `update_where`. @samukweku
+-   [ENH] Add clearer error messages for `pivot_longer`. h/t to @tdhock
+    for the observation. -@samukweku
+-   [ENH] `select_columns` now uses variable arguments (*args),
+    to provide a simpler selection without the need for lists. - @samukweku
+-   [ENH] `encode_categoricals` refactored to use generic functions
+    via `functools.dispatch`. - @samukweku
 
 ## [v0.20.14] - 2021-03-25
 
diff --git a/janitor/functions.py b/janitor/functions.py
index b847042..17512ed 100644
--- a/janitor/functions.py
+++ b/janitor/functions.py
@@ -1,6 +1,5 @@
 """ General purpose data cleaning functions. """
 
-import collections
 import datetime as dt
 import inspect
 import re
@@ -52,6 +51,7 @@ from .utils import (
     check,
     check_column,
     deprecated_alias,
+    asCategorical,
 )
 
 
@@ -469,26 +469,19 @@ def get_dupes(
 
 
 def As_Categorical(
-    categories: Optional[
-        Union[List, Set, Tuple, pd.Series, np.ndarray]
-    ] = None,
-    order: Optional[str] = None,
+    categories: Optional[List] = None, order: Optional[str] = None,
 ) -> NamedTuple:
     """
     Helper function for `encode_categorical`. It makes creating the
     `categories` and `order` more explicit. Inspired by pd.NamedAgg.
-
     :param categories: list-like object to create new categorical column.
     :param order: string object that can be either "sort" or "appearance".
         If "sort", the `categories` argument will be sorted with np.sort;
         if "apperance", the `categories` argument will be used as is.
     :returns: A namedtuple of (`categories`, `order`).
     """
-    AsCategorical = collections.namedtuple(
-        "AsCategorical", ["categories", "order"], defaults=(None, None)
-    )
 
-    return AsCategorical._make((categories, order))
+    return asCategorical(categories=categories, order=order)
 
 
 @pf.register_dataframe_method
@@ -686,7 +679,7 @@ def encode_categorical(
             .encode_categorical(
                 col1 = (categories, order),
                 col2 = jn.As_Categorical(
-                            categories = [values],
+                            categories = [values]/None,
                             order="sort"/"appearance"/None
                             )
         )
@@ -1463,9 +1456,7 @@ def deconcatenate_column(
         for i, col_new in enumerate(new_column_names):
             cols.insert(index_original + i, col_new)
 
-        df_new = df_new.select_columns(search_column_names=cols).drop(
-            columns=column_name
-        )
+        df_new = df_new.select_columns(cols).drop(columns=column_name)
 
     return df_new
 
@@ -3142,9 +3133,7 @@ def currency_column_to_numeric(
 @pf.register_dataframe_method
 @deprecated_alias(search_cols="search_column_names")
 def select_columns(
-    df: pd.DataFrame,
-    search_column_names: Union[str, callable, Pattern, slice, list],
-    invert: bool = False,
+    df: pd.DataFrame, *args, invert: bool = False,
 ) -> pd.DataFrame:
     """
     Method-chainable selection of columns.
@@ -3255,15 +3244,16 @@ def select_columns(
         0    4.0     8     E     T     a 2018-01-01
         1    NaN     5   NaN     U     b 2018-01-01
 
-    - Select via a list (you can combine any of the previous options)::
+    - Select a combination of the above
+      (you can combine any of the previous options)::
 
-        df.select_columns(["id", "code*", slice("code", "code2")])
+        df.select_columns("id", "code*", slice("code", "code2"))
 
            id  code  code1 code2 code3
         0   0     1    4.0     8     a
         1   1     2    NaN     5     b
 
-    - You can also pass a list of booleans::
+    - You can also pass a sequence of booleans::
 
         df.select_columns([True, False, True, True, True,
                            False, False, False, True, False])
@@ -3275,7 +3265,7 @@ def select_columns(
     - Setting ``invert`` to ``True``
       returns the complement of the columns provided::
 
-        df.select_columns(["id", "code*", slice("code", "code2")],
+        df.select_columns("id", "code*", slice("code", "code2"),
                           invert = True)
 
            Name type type1 type2      type3
@@ -3289,7 +3279,7 @@ def select_columns(
 
        df = pd.DataFrame(...)
 
-       df = jn.select_columns(['a', 'b', 'col_*'],
+       df = jn.select_columns('a', 'b', 'col_*',
                               invert=True)
 
     Method-chaining example:
@@ -3297,18 +3287,19 @@ def select_columns(
     .. code-block:: python
 
         df = (pd.DataFrame(...)
-              .select_columns(['a', 'b', 'col_*'],
+              .select_columns('a', 'b', 'col_*',
               invert=True))
 
 
     :param df: A pandas DataFrame.
-    :param search_column_names: Valid inputs include:
+    :param args: Valid inputs include:
 
         - an exact column name to look for
         - a shell-style glob string (e.g., `*_thing_*`)
         - a regular expression
         - a callable which is applicable to each Series in the dataframe
-        - a list of all the aforementioned options.
+        - variable arguments of all the aforementioned.
+        - a sequence of booleans.
     :param invert: Whether or not to invert the selection.
         This will result in the selection of the complement of the columns
         provided.
@@ -3323,10 +3314,14 @@ def select_columns(
     # applicable for any
     # 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]
+    search_column_names = []
+    for arg in args:
+        if is_list_like(arg) and (not isinstance(arg, tuple)):
+            search_column_names.extend([*arg])
+        else:
+            search_column_names.append(arg)
+    if len(search_column_names) == 1:
+        search_column_names = search_column_names[0]
 
     full_column_list = _select_columns(search_column_names, df)
 
diff --git a/janitor/utils.py b/janitor/utils.py
index 8273582..2c23f6d 100644
--- a/janitor/utils.py
+++ b/janitor/utils.py
@@ -7,7 +7,6 @@ import re
 import sys
 
 import warnings
-from collections import namedtuple
 from collections.abc import Callable as dispatch_callable
 from itertools import chain, combinations
 from typing import (
@@ -19,6 +18,7 @@ from typing import (
     Pattern,
     Tuple,
     Union,
+    NamedTuple,
 )
 
 import numpy as np
@@ -1116,7 +1116,8 @@ def _data_checks_pivot_longer(
             if ".value" in names_to:
                 raise ValueError(
                     """
-                    ``.value`` not accepted if ``names_pattern``
+                    ``.value`` is not accepted
+                    if ``names_pattern``
                     is a list/tuple.
                     """
                 )
@@ -1126,11 +1127,8 @@ def _data_checks_pivot_longer(
 
     check("values_to", values_to, [str])
 
-    if (values_to in df.columns) and any(
-        (
-            ".value" not in names_to,
-            not isinstance(names_pattern, (list, tuple)),
-        )
+    if (values_to in df.columns) and not any(
+        (".value" in names_to, isinstance(names_pattern, (list, tuple)),)
     ):
         # copied from pandas' melt source code
         # with a minor tweak
@@ -1288,18 +1286,22 @@ def _pivot_longer_extractions(
         if mapping.isna().all(axis=None):
             raise ValueError(
                 """
-                The regular expression in ``names_pattern``
-                did not return any matches.
+                No labels in the columns
+                matched the regular expression
+                in ``names_pattern``.
+                Kindly provide a regular expression
+                that matches all labels in the columns.
                 """
             )
 
         if 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.
+                Not all labels in the columns
+                matched the regular expression
+                in ``names_pattern``.
+                Kindly provide a regular expression
+                that matches all labels in the columns.
                 """
             )
 
@@ -1324,8 +1326,11 @@ def _pivot_longer_extractions(
         if not np.any(mapping):
             raise ValueError(
                 """
-                The regular expressions in ``names_pattern``
-                did not return any matches.
+                Not all labels in the columns
+                matched the regular expression
+                in ``names_pattern``.
+                Kindly provide a regular expression
+                that matches all labels in the columns.
                 """
             )
 
@@ -1774,136 +1779,220 @@ def _computations_pivot_wider(
     return df
 
 
-def _computations_as_categorical(df: pd.DataFrame, **kwargs) -> pd.DataFrame:
+class asCategorical(NamedTuple):
     """
-    This function handles cases where categorical columns are created with
-    an order, or specific values supplied for the categories. It uses a kwarg,
-    with a namedtuple - `column_name: (categories, order)`, with the idea
-    inspired by Pandas' NamedAggregation. The defaults for the namedtuple are
-    (None, None) and will return a categorical dtype with no order and
-    categories inferred from the column.
+    Helper class for `encode_categorical`. It makes creating the
+    `categories` and `order` more explicit. Inspired by pd.NamedAgg.
+
+    :param categories: list-like object to create new categorical column.
+    :param order: string object that can be either "sort" or "appearance".
+        If "sort", the `categories` argument will be sorted with np.sort;
+        if "apperance", the `categories` argument will be used as is.
+    :returns: A namedtuple of (`categories`, `order`).
     """
 
-    AsCategorical = namedtuple(
-        "AsCategorical", ["categories", "order"], defaults=(None, None)
-    )
+    categories: list = None
+    order: str = None
 
-    categories_dict = {}
 
-    # type and column presence checks
-    check_column(df, kwargs)
+def as_categorical_checks(df: pd.DataFrame, **kwargs) -> tuple:
+    """
+    This function raises errors if columns in `kwargs` are
+    absent in the the dataframe's columns.
+    It also raises errors if the tuple in `kwargs`
+    has a length greater than 2, or the `order` value,
+    if not None, is not one of `appearance` or `sort`.
+    Error is raised if the `categories` in the tuple in `kwargs`
+    is not a 1-D array-like object.
 
-    for column_name, value in kwargs.items():
-        check("AsCategorical", value, [tuple])
-        if len(value) != 2:
-            raise ValueError("Must provide tuples of (categories, order).")
+    This function is executed before proceeding to the computation phase.
 
-        value = AsCategorical._make(value)
+    If all checks pass, the dataframe,
+    and a pairing of column names and namedtuple
+    of (categories, order) is returned.
 
-        if value.categories is not None:
-            check(
-                "categories",
-                value.categories,
-                [list, tuple, set, np.ndarray, pd.Series],
-            )
+    :param df: The pandas DataFrame object.
+    :param kwargs: A pairing of column name
+        to a tuple of (`categories`, `order`).
+    :returns: A tuple (pandas DataFrame, dictionary).
+    :raises TypeError: if ``kwargs`` is not a tuple.
+    :raises ValueError: if ``categories`` is not a 1-D array.
+    :raises ValueError: if ``order`` is not one of
+        `sort`, `appearance`, or `None`.
+    """
+
+    # column checks
+    check_column(df, kwargs)
+
+    categories_dict = {}
 
-        if value.order is not None:
-            check("order", value.order, [str])
-            if value.order not in ("appearance", "sort"):
+    # type checks
+    for column_name, value in kwargs.items():
+        check("Pair of `categories` and `order`", value, [tuple])
+        if len(value) != 2:
+            raise ValueError("Must provide tuple of (categories, order).")
+        value = asCategorical(*value)
+        value_categories = value.categories
+        if value_categories is not None:
+            if not is_list_like(value_categories):
+                raise TypeError(f"{value_categories} should be list-like.")
+            value_categories = [*value_categories]
+            arr_ndim = np.asarray(value_categories).ndim
+            if any((arr_ndim < 1, arr_ndim > 1)):
+                raise ValueError(
+                    f"""
+                    {value_categories} is not a 1-D array.
+                    """
+                )
+        value_order = value.order
+        if value_order is not None:
+            check("order", value_order, [str])
+            if value_order not in ("appearance", "sort"):
                 raise ValueError(
                     """
                     `order` argument should be one of
                     "appearance", "sort" or `None`.
                     """
                 )
+        categories_dict[column_name] = asCategorical(
+            categories=value_categories, order=value_order
+        )
 
-        categories_dict[column_name] = value
+    return df, categories_dict
 
-    categories_dtypes = {}
-    unique_values_in_column = None
-    missing_values = None
-    for column_name, categories_order_tuple in categories_dict.items():
-        if categories_order_tuple.categories is None:
-            if categories_order_tuple.order is None:
-                categories_dtypes[column_name] = "category"
-
-            elif categories_order_tuple.order == "sort":
-                if df[column_name].hasnans:
-                    unique_values_in_column = np.unique(
-                        df[column_name].dropna()
-                    )
-                else:
-                    unique_values_in_column = np.unique(df[column_name])
-                categories_dtypes[column_name] = CategoricalDtype(
-                    categories=unique_values_in_column, ordered=True
-                )
 
-            else:  # appearance
-                if df[column_name].hasnans:
-                    unique_values_in_column = df[column_name].dropna().unique()
-                else:
-                    unique_values_in_column = df[column_name].unique()
-                categories_dtypes[column_name] = CategoricalDtype(
-                    categories=unique_values_in_column, ordered=True
-                )
-        # categories supplied
-        else:
-            if df[column_name].hasnans:
-                unique_values_in_column = df[column_name].dropna().unique()
-            else:
-                unique_values_in_column = df[column_name].unique()
-            missing_values = np.setdiff1d(
-                unique_values_in_column,
-                pd.unique(categories_order_tuple.categories),
-                assume_unique=True,
-            )
-            # check if categories supplied does not match
-            # with the values in the column
-            # either there are no matches
-            # or an incomplete number of matches
-            if np.any(missing_values):
-                if len(missing_values) == len(unique_values_in_column):
-                    warnings.warn(
-                        f"""
-                        None of the values in {column_name} are in
-                        {categories_order_tuple.categories};
-                        this might create nulls for all your values
-                        in the new categorical column.
-                        """,
-                        UserWarning,
-                        stacklevel=2,
-                    )
-                else:
-                    warnings.warn(
-                        f"""
-                        Values {tuple(missing_values)} are missing from
-                        categories {categories_order_tuple.categories}
-                        for {column_name}; this may create nulls
-                        the new categorical column.
-                        """,
-                        UserWarning,
-                        stacklevel=2,
-                    )
-
-            if categories_order_tuple.order is None:
-                categories_dtypes[column_name] = CategoricalDtype(
-                    categories=categories_order_tuple.categories, ordered=False
-                )
-            elif categories_order_tuple.order == "sort":
-                categories_dtypes[column_name] = CategoricalDtype(
-                    categories=np.sort(categories_order_tuple.categories),
-                    ordered=True,
-                )
-            else:  # appearance
-                categories_dtypes[column_name] = CategoricalDtype(
-                    categories=categories_order_tuple.categories, ordered=True
-                )
+def _computations_as_categorical(df: pd.DataFrame, **kwargs) -> pd.DataFrame:
+    """
+    This function handles cases where categorical columns are created with
+    an order, or specific values supplied for the categories. It uses a kwarg,
+    with a namedtuple - `column_name: (categories, order)`, with the idea
+    inspired by Pandas' NamedAggregation. The defaults for the namedtuple are
+    (None, None) and will return a categorical dtype with no order and
+    categories inferred from the column.
+    """
+
+    df, categories_dict = as_categorical_checks(df, **kwargs)
+
+    categories_dtypes = {}
+    for column_name, ascategorical in categories_dict.items():
+        categories = _encode_categories(
+            ascategorical.categories, df, column_name
+        )
+        categories_dtypes[column_name] = _encode_order(
+            ascategorical.order, categories
+        )
 
     df = df.astype(categories_dtypes)
 
     return df
 
 
+@functools.singledispatch
+def _encode_categories(cat, df, column):
+    """
+    base function for processing `categories`
+    in `_computations_as_categorical`.
+    Returns a Series.
+    """
+    raise TypeError("This type is not supported in `categories`.")
+
+
+@_encode_categories.register(type(None))  # noqa: F811
+def _sub_categories(cat, df, column):  # noqa: F811
+    """
+    base function for processing `categories`
+    in `_computations_as_categorical`.
+    Apllies to only NoneType.
+    Returns a Series.
+    """
+    column = df[column]
+    if column.hasnans:
+        column = column.dropna()
+    return column
+
+
+@_encode_categories.register(list)  # noqa: F811
+def _sub_categories(cat, df, column):  # noqa: F811
+    """
+    base function for processing `categories`
+    in `_computations_as_categorical`.
+    Apllies to only list type.
+    Returns a Series.
+    """
+    if pd.isna(cat).any():
+        raise ValueError("""`categories` cannot have null values.""")
+    col = df[column]
+    check_presence = col.isin(cat)
+    check_presence_sum = check_presence.sum()
+
+    if check_presence_sum == 0:
+        warnings.warn(
+            f"""
+            None of the values in `{column}` are in
+            {cat};
+            this might create nulls for all your values
+            in the new categorical column.
+            """,
+            UserWarning,
+            stacklevel=2,
+        )
+    elif check_presence_sum != check_presence.size:
+        missing_values = col[~check_presence]
+        if missing_values.hasnans:
+            missing_values = missing_values.dropna()
+        warnings.warn(
+            f"""
+            Values {tuple(missing_values)} are missing from
+            categories {cat}
+            for {column}; this may create nulls
+            in the new categorical column.
+            """,
+            UserWarning,
+            stacklevel=2,
+        )
+    return pd.Series(cat)
+
+
+@functools.singledispatch
+def _encode_order(order, categories):
+    """
+    base function for processing `order`
+    in `_computations_as_categorical`.
+    Returns a pd.CategoricalDtype().
+    """
+    raise TypeError("This type is not supported in `order`.")
+
+
+@_encode_order.register(type(None))  # noqa: F811
+def _sub_encode_order(order, categories):  # noqa: F811
+    """
+    base function for processing `order`
+    in `_computations_as_categorical`.
+    Apllies to only NoneType.
+    Returns a pd.CategoricalDtype().
+    """
+    if not categories.is_unique:
+        categories = categories.unique()
+
+    return pd.CategoricalDtype(categories=categories, ordered=False)
+
+
+@_encode_order.register(str)  # noqa: F811
+def _sub_encode_order(order, categories):  # noqa: F811
+    """
+    base function for processing `order`
+    in `_computations_as_categorical`.
+    Apllies to only strings.
+    Returns a pd.CategoricalDtype().
+    """
+    if (order == "sort") and (not categories.is_monotonic_increasing):
+        categories = categories.sort_values()
+    if not categories.is_unique:
+        categories = categories.unique()
+
+    return pd.CategoricalDtype(categories=categories, ordered=True)
+
+
 @functools.singledispatch
 def _select_columns(columns_to_select, df):
     """


Test patch:
diff --git a/tests/functions/test_encode_categorical.py b/tests/functions/test_encode_categorical.py
index 079a9ba..7a7333d 100644
--- a/tests/functions/test_encode_categorical.py
+++ b/tests/functions/test_encode_categorical.py
@@ -55,6 +55,17 @@ def test_encode_categorical_invalid_input(df):
         df.encode_categorical(1)
 
 
+@pytest.mark.functions
+@given(df=df_strategy())
+def test_encode_categorical_invalid_input_2(df):
+    """
+    Raise JanitorError for wrong input type
+    for `column_names`.
+    """
+    with pytest.raises(JanitorError):
+        df.encode_categorical({"names"})
+
+
 @pytest.fixture
 def df_categorical():
     return pd.DataFrame(
@@ -113,7 +124,27 @@ def test_categories_type_in_kwargs(df_categorical):
     the `categories` parameter in kwargs.
     """
     with pytest.raises(TypeError):
-        df_categorical.encode_categorical(col1=({1: 2, 3: 3}, None))
+        df_categorical.encode_categorical(col1=("category", None))
+
+
+def test_categories_ndim_gt_1_in_kwargs(df_categorical):
+    """
+    Raise ValueError if the argument supplied to
+    the `categories` parameter in kwargs is not
+    1-D array-like.
+    """
+    arrays = [[1, 1, 2, 2], ["red", "blue", "red", "blue"]]
+    arrays = pd.MultiIndex.from_arrays(arrays, names=("number", "color"))
+    with pytest.raises(ValueError):
+        df_categorical.encode_categorical(col1=(arrays, None))
+
+
+def test_categories_null_in_categories(df_categorical):
+    """
+     Raise ValueError if there are nulls in the `categories`.
+    """
+    with pytest.raises(ValueError):
+        df_categorical.encode_categorical(col1=([None, 2, 3], None))
 
 
 def test_order_type_in_kwargs(df_categorical):
@@ -385,7 +416,17 @@ def test_various(df_in, df_out, kwargs):
     Test output for various combinations.
     """
     result = df_in.encode_categorical(**kwargs,)
-    assert_frame_equal(result, df_out)
+    # directly comparing columns is safe -
+    # if the columns have differing categories
+    # (especially for ordered True) it will fail.
+    # if both categories are unordered, then the
+    # order is not considered.
+    # comparing with assert_frame_equal fails
+    # for unordered categoricals, as internally
+    # the order of the categories are compared
+    # which is irrelevant for unordered categoricals
+    for key, _ in kwargs.items():
+        assert df_out[key].equals(result[key])
 
 
 df_warnings = [
diff --git a/tests/functions/test_select_columns.py b/tests/functions/test_select_columns.py
index f6d621a..c06e497 100644
--- a/tests/functions/test_select_columns.py
+++ b/tests/functions/test_select_columns.py
@@ -13,7 +13,7 @@ from pandas.testing import assert_frame_equal
 )
 def test_select_columns(dataframe, invert, expected):
     columns = ["a", "Bell__Chart", "cities"]
-    df = dataframe.select_columns(search_column_names=columns, invert=invert)
+    df = dataframe.select_columns(columns, invert=invert)
 
     assert_frame_equal(df, dataframe[expected])
 
@@ -28,7 +28,7 @@ def test_select_columns(dataframe, invert, expected):
 )
 def test_select_columns_glob_inputs(dataframe, invert, expected):
     columns = ["Bell__Chart", "a*"]
-    df = dataframe.select_columns(search_column_names=columns, invert=invert)
+    df = dataframe.select_columns(columns, invert=invert)
 
     assert_frame_equal(df, dataframe[expected])
 
@@ -46,7 +46,7 @@ def test_select_columns_glob_inputs(dataframe, invert, expected):
 def test_select_columns_missing_columns(dataframe, columns):
     """Check that passing non-existent column names or search strings raises KeyError"""  # noqa: E501
     with pytest.raises(KeyError):
-        dataframe.select_columns(search_column_names=columns)
+        dataframe.select_columns(columns)
 
 
 @pytest.mark.functions
@@ -76,7 +76,7 @@ def test_select_columns_missing_columns(dataframe, columns):
 def test_select_columns_input(dataframe, columns):
     """Check that passing an iterable that is not a list raises TypeError."""
     with pytest.raises(TypeError):
-        dataframe.select_columns(search_column_names=columns)
+        dataframe.select_columns(columns)
 
 
 @pytest.mark.functions
@@ -90,7 +90,7 @@ def test_select_columns_input(dataframe, columns):
 def test_select_unique_columns(dataframe, invert, expected):
     """Test that only unique columns are returned."""
     columns = ["Bell__*", slice("a", "decorated-elephant")]
-    df = dataframe.select_columns(search_column_names=columns, invert=invert)
+    df = dataframe.select_columns(columns, invert=invert)
 
     assert_frame_equal(df, dataframe[expected])
 
@@ -109,7 +109,7 @@ def test_select_callable_columns(dataframe, invert, expected):
     def columns(x):
         return "-" in x.name or "_" in x.name
 
-    df = dataframe.select_columns(search_column_names=columns, invert=invert)
+    df = dataframe.select_columns(columns, invert=invert)
 
     assert_frame_equal(df, dataframe[expected])