Repository: dask/dask

Issue:
Cannot use .str methods on categorical columns
If I try to use the `.str` methods on a categorical column in a dask.dataframe, I get an exception.  These methods work on pandas categorical columns, so I would expect them here.

Sample code:
``` python
import pandas as pd
import dask.dataframe as dd

df = pd.DataFrame(dict(x=pd.Series([1,2,3,4,5]),
                       y=pd.Series(['aaa', 'bba', 'bbb', 'bbc', 'ccc'], dtype='category')))
dask_df = dd.from_pandas(df, npartitions=1)

# Pandas
print(df.y.str.endswith('a'))
# Dask
print(dask_df.y.str.endswith('a').compute())
```
Produces the following:
```
0     True
1     True
2    False
3    False
4    False
Name: y, dtype: bool
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-38-02611a41e679> in <module>()
      9 print(df.y.str.endswith('a'))
     10 # Dask
---> 11 print(dask_df.y.str.endswith('a').compute())

pandas/_libs/src/properties.pyx in pandas._libs.lib.cache_readonly.__get__ (pandas/_libs/lib.c:44442)()

~/anaconda/envs/intake/lib/python3.6/site-packages/dask/dataframe/core.py in str(self)
   1627     @cache_readonly
   1628     def str(self):
-> 1629         return StringAccessor(self)
   1630 
   1631     def __dir__(self):

~/anaconda/envs/intake/lib/python3.6/site-packages/dask/dataframe/accessor.py in __init__(self, series)
     29         if not isinstance(series, Series):
     30             raise ValueError('Accessor cannot be initialized')
---> 31         self._validate(series)
     32         self._series = series
     33 

~/anaconda/envs/intake/lib/python3.6/site-packages/dask/dataframe/accessor.py in _validate(self, series)
    100     def _validate(self, series):
    101         if not series.dtype == 'object':
--> 102             raise AttributeError("Can only use .str accessor with object dtype")

AttributeError: Can only use .str accessor with object dtype
```

pandas: 0.20.1, dask: 0.15.1


Discussion:

Resolving patch:
diff --git a/dask/dataframe/accessor.py b/dask/dataframe/accessor.py
--- a/dask/dataframe/accessor.py
+++ b/dask/dataframe/accessor.py
@@ -5,6 +5,7 @@
 from toolz import partial
 
 from ..utils import derived_from
+from .utils import is_categorical_dtype
 
 
 def maybe_wrap_pandas(obj, x):
@@ -109,7 +110,9 @@ class StringAccessor(Accessor):
     _not_implemented = {'get_dummies'}
 
     def _validate(self, series):
-        if not series.dtype == 'object':
+        if not (series.dtype == 'object' or (
+                is_categorical_dtype(series) and
+                series.cat.categories.dtype == 'object')):
             raise AttributeError("Can only use .str accessor with object dtype")
 
     @derived_from(pd.core.strings.StringMethods)


Test patch:
diff --git a/dask/dataframe/tests/test_categorical.py b/dask/dataframe/tests/test_categorical.py
--- a/dask/dataframe/tests/test_categorical.py
+++ b/dask/dataframe/tests/test_categorical.py
@@ -342,3 +342,16 @@ def test_unknown_categories(self, series):
         res = db.compute()
         tm.assert_index_equal(db.cat.categories, get_cat(res).categories)
         assert_array_index_eq(db.cat.codes, get_cat(res).codes)
+
+    def test_categorical_string_ops(self):
+        a = pd.Series(['a', 'a', 'b'], dtype='category')
+        da = dd.from_pandas(a, 2)
+        result = da.str.upper()
+        expected = a.str.upper()
+        assert_eq(result, expected)
+
+    def test_categorical_non_string_raises(self):
+        a = pd.Series([1, 2, 3], dtype='category')
+        da = dd.from_pandas(a, 2)
+        with pytest.raises(AttributeError):
+            da.str.upper()