Repository: pandas-dev/pandas
Pull request: ENH - Modify Dataframe.select_dtypes to accept scalar values
This commit relates to GH16855. It allows the Dataframe.select_dtypes
function to accept scalar values as well as list-like values. As such,
it should maintain backwards compatibility.

 - [x ] closes #16855
 - [x ] tests added / passed
 - [x ] passes ``git diff upstream/master --name-only -- '*.py' | flake8 --diff`` (On Windows, ``git diff upstream/master -u -- "*.py" | flake8 --diff`` might work as an alternative.)
 - [x ] whatsnew entry


Problem statement:
API: DataFrame.select_dtypes should accept scalar
```
In [164]: df = pd.DataFrame({'a': [1, 2, 3], 'b': ['a', 'b', 'c']})

In [165]: df.select_dtypes(include='object')
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-165-04044faa1a5a> in <module>()
----> 1 df.select_dtypes(include='object')

~\AppData\Local\Continuum\Anaconda3\lib\site-packages\pandas\core\frame.py in select_dtypes(self, include, exclude)
   2355         include, exclude = include or (), exclude or ()
   2356         if not (is_list_like(include) and is_list_like(exclude)):
-> 2357             raise TypeError('include and exclude must both be non-string'
   2358                             ' sequences')
   2359         selection = tuple(map(frozenset, (include, exclude)))

TypeError: include and exclude must both be non-string sequences

In [166]: df.select_dtypes(include=['object'])
Out[166]: 
   b
0  a
1  b
2  c
```
#### Problem description

Only a convenience thing, but basically anywhere else we take list-likes, we accept a single string and I think should do the same here.

`pandas 0.20.2`

Hints from issue discussion:
+100 :) We should do the same for `exclude`
I was looking at picking this up as one of my first contributions. A quick question for clarity though.

`select_dtypes` allows strings (`'category'`, `'datetimetz'`, etc) but also allows `numpy.number`. For example:

    df.select_dtypes(include=['category', numpy.number])

is valid.

My question is: what's the expected behaviour for `df.select_dtypes(include=np.number)`? The original issue only mentions allowing strings but it seems silly to exclude `np.number`, as such I'll continue by assuming that's the way to go but would be good to get some clarity.
Correct. It should convert any scalar type to an iterable. 

> On Jul 8, 2017, at 07:56, Keiron Pizzey <notifications@github.com> wrote:
> 
> I was looking at picking this up as one of my first contributions. A quick question for clarity though.
> 
> select_dtypes allows strings ('category', 'datetimetz', etc) but also allows numpy.number. For example:
> 
> df.select_dtypes(include=['category', numpy.number])
> is valid.
> 
> My question is: what's the expected behaviour for df.select_dtypes(include=np.number)? The original issue only mentions allowing strings but it seems silly to exclude np.number, as such I'll continue by assuming that's the way to go but would be good to get some clarity.
> 
> —
> You are receiving this because you commented.
> Reply to this email directly, view it on GitHub, or mute the thread.
>

Commit message: ENH - Modify Dataframe.select_dtypes to accept scalar values

This commit related to GH16855. It allows the Dataframe.select_dtypes
function to accept scalar values as well as list-like values. As such,
it should maintain backwards compatibility.

Patch to review:
diff --git a/doc/source/basics.rst b/doc/source/basics.rst
index 134cc5106015b..d8b1602fb104d 100644
--- a/doc/source/basics.rst
+++ b/doc/source/basics.rst
@@ -2229,7 +2229,3 @@ All numpy dtypes are subclasses of ``numpy.generic``:
 
     Pandas also defines the types ``category``, and ``datetime64[ns, tz]``, which are not integrated into the normal
     numpy hierarchy and wont show up with the above function.
-
-.. note::
-
-   The ``include`` and ``exclude`` parameters must be non-string sequences.
diff --git a/doc/source/style.ipynb b/doc/source/style.ipynb
index 4eeda491426b1..c250787785e14 100644
--- a/doc/source/style.ipynb
+++ b/doc/source/style.ipynb
@@ -935,7 +935,7 @@
     "\n",
     "<span style=\"color: red\">*Experimental: This is a new feature and still under development. We'll be adding features and possibly making breaking changes in future releases. We'd love to hear your feedback.*</span>\n",
     "\n",
-    "Some support is available for exporting styled `DataFrames` to Excel worksheets using the `OpenPyXL` engine. CSS2.2 properties handled include:\n",
+    "Some support is available for exporting styled `DataFrames` to Excel worksheets using the `OpenPyXL` engine. CSS2.2 properties handled include:\n",
     "\n",
     "- `background-color`\n",
     "- `border-style`, `border-width`, `border-color` and their {`top`, `right`, `bottom`, `left` variants}\n",
diff --git a/doc/source/whatsnew/v0.21.0.txt b/doc/source/whatsnew/v0.21.0.txt
index d5cc3d6ddca8e..6968bbebc836c 100644
--- a/doc/source/whatsnew/v0.21.0.txt
+++ b/doc/source/whatsnew/v0.21.0.txt
@@ -39,6 +39,7 @@ Other Enhancements
 - :func:`read_feather` has gained the ``nthreads`` parameter for multi-threaded operations (:issue:`16359`)
 - :func:`DataFrame.clip()` and :func:`Series.clip()` have gained an ``inplace`` argument. (:issue:`15388`)
 - :func:`crosstab` has gained a ``margins_name`` parameter to define the name of the row / column that will contain the totals when ``margins=True``. (:issue:`15972`)
+- :func:`Dataframe.select_dtypes` now accepts scalar values for include/exclude as well as list-like. (:issue:`16855`)
 
 .. _whatsnew_0210.api_breaking:
 
diff --git a/pandas/core/frame.py b/pandas/core/frame.py
index 80cdebc24c39d..9e7a1ec805ab7 100644
--- a/pandas/core/frame.py
+++ b/pandas/core/frame.py
@@ -2285,9 +2285,9 @@ def select_dtypes(self, include=None, exclude=None):
 
         Parameters
         ----------
-        include, exclude : list-like
-            A list of dtypes or strings to be included/excluded. You must pass
-            in a non-empty sequence for at least one of these.
+        include, exclude : scalar or list-like
+            A selection of dtypes or strings to be included/excluded. At least
+            one of these parameters must be supplied.
 
         Raises
         ------
@@ -2295,8 +2295,6 @@ def select_dtypes(self, include=None, exclude=None):
             * If both of ``include`` and ``exclude`` are empty
             * If ``include`` and ``exclude`` have overlapping elements
             * If any kind of string dtype is passed in.
-        TypeError
-            * If either of ``include`` or ``exclude`` is not a sequence
 
         Returns
         -------
@@ -2348,10 +2346,15 @@ def select_dtypes(self, include=None, exclude=None):
         4   True
         5  False
         """
-        include, exclude = include or (), exclude or ()
-        if not (is_list_like(include) and is_list_like(exclude)):
-            raise TypeError('include and exclude must both be non-string'
-                            ' sequences')
+
+        # GH16855 - If either include or exclude is a non-None scalar then
+        # convert to a tuple of length 1 and continue.
+        # This allows, for example, df.select_dtypes(include='object').
+        if not is_list_like(include):
+            include = (include,) if include is not None else ()
+        if not is_list_like(exclude):
+            exclude = (exclude,) if exclude is not None else ()
+
         selection = tuple(map(frozenset, (include, exclude)))
 
         if not any(selection):
diff --git a/pandas/tests/frame/test_dtypes.py b/pandas/tests/frame/test_dtypes.py
index 335b76ff2aade..1ef0025ad43fc 100644
--- a/pandas/tests/frame/test_dtypes.py
+++ b/pandas/tests/frame/test_dtypes.py
@@ -149,6 +149,10 @@ def test_select_dtypes_include(self):
         ei = df[['k']]
         assert_frame_equal(ri, ei)
 
+        ri = df.select_dtypes(include='category')
+        ei = df[['f']]
+        assert_frame_equal(ri, ei)
+
         pytest.raises(NotImplementedError,
                       lambda: df.select_dtypes(include=['period']))
 
@@ -162,6 +166,10 @@ def test_select_dtypes_exclude(self):
         ee = df[['a', 'e']]
         assert_frame_equal(re, ee)
 
+        re = df.select_dtypes(exclude=np.number)
+        ee = df[['a', 'e']]
+        assert_frame_equal(re, ee)
+
     def test_select_dtypes_exclude_include(self):
         df = DataFrame({'a': list('abc'),
                         'b': list(range(1, 4)),
@@ -181,6 +189,12 @@ def test_select_dtypes_exclude_include(self):
         e = df[['b', 'e']]
         assert_frame_equal(r, e)
 
+        exclude = 'datetime'
+        include = 'bool'
+        r = df.select_dtypes(include=include, exclude=exclude)
+        e = df[['e']]
+        assert_frame_equal(r, e)
+
     def test_select_dtypes_not_an_attr_but_still_valid_dtype(self):
         df = DataFrame({'a': list('abc'),
                         'b': list(range(1, 4)),
@@ -205,18 +219,6 @@ def test_select_dtypes_empty(self):
                                     'must be nonempty'):
             df.select_dtypes()
 
-    def test_select_dtypes_raises_on_string(self):
-        df = DataFrame({'a': list('abc'), 'b': list(range(1, 4))})
-        with tm.assert_raises_regex(TypeError, 'include and exclude '
-                                    '.+ non-'):
-            df.select_dtypes(include='object')
-        with tm.assert_raises_regex(TypeError, 'include and exclude '
-                                    '.+ non-'):
-            df.select_dtypes(exclude='object')
-        with tm.assert_raises_regex(TypeError, 'include and exclude '
-                                    '.+ non-'):
-            df.select_dtypes(include=int, exclude='object')
-
     def test_select_dtypes_bad_datetime64(self):
         df = DataFrame({'a': list('abc'),
                         'b': list(range(1, 4)),


Code review comments:
diff_hunk: @@ -2348,10 +2346,15 @@ def select_dtypes(self, include=None, exclude=None):
         4   True
         5  False
         """
-        include, exclude = include or (), exclude or ()
-        if not (is_list_like(include) and is_list_like(exclude)):
-            raise TypeError('include and exclude must both be non-string'
-                            ' sequences')
+
+        # GH16855 - If either include or exclude is a non-None scalar then
path: pandas/core/frame.py
text: @user1:
comment not needed