Repository: dask/dask

Issue:
DateTime Conversion/General Column Conversion
Dask currently has no methods to convert dtype of an existing column.   These is fairly common:

```
pd.to_datetime()
df.date.astype(np.datetime64)
```

I had to reach a bit to get similar behavior:

```
ddf = ddf.assign(nDate=dd.Date.map(np.datetime64))
```



Discussion:
I think this could be implemented using `elemwise`.


Resolving patch:
diff --git a/dask/dataframe/core.py b/dask/dataframe/core.py
--- a/dask/dataframe/core.py
+++ b/dask/dataframe/core.py
@@ -289,6 +289,22 @@ def __getstate__(self):
     def __setstate__(self, dict):
         self.__dict__ = dict
 
+    @wraps(pd.Series.fillna)
+    def fillna(self, value):
+        func = getattr(self._partition_type, 'fillna')
+        return map_partitions(func, self.column_info, self, value)
+
+    def sample(self, frac):
+        """ Random sample of items
+
+        This only implements the ``frac`` option from pandas.
+
+        See Also:
+            pd.DataFrame.sample
+        """
+        func = getattr(self._partition_type, 'sample')
+        return map_partitions(func, self.column_info, self, None, frac)
+
 
 class Series(_Frame):
     """ Out-of-core Series object
@@ -307,6 +323,8 @@ def __init__(self, dsk, _name, name, divisions):
         self._name = _name
         self.name = name
         self.divisions = tuple(divisions)
+        self.dt = DatetimeAccessor(self)
+        self.str = StringAccessor(self)
 
     @property
     def _args(self):
@@ -466,6 +484,27 @@ def isin(self, other):
     def map(self, arg, na_action=None):
         return elemwise(pd.Series.map, self, arg, na_action, name=self.name)
 
+    @wraps(pd.Series.astype)
+    def astype(self, dtype):
+        return map_partitions(pd.Series.astype, self.name, self, dtype)
+
+    @wraps(pd.Series.dropna)
+    def dropna(self):
+        return map_partitions(pd.Series.dropna, self.name, self)
+
+    @wraps(pd.Series.between)
+    def between(self, left, right, inclusive=True):
+        return map_partitions(pd.Series.between, self.name, self, left, right,
+                inclusive)
+
+    @wraps(pd.Series.clip)
+    def clip(self, lower=None, upper=None):
+        return map_partitions(pd.Series.clip, self.name, self, lower, upper)
+
+    @wraps(pd.Series.notnull)
+    def notnull(self):
+        return map_partitions(pd.Series.notnull, self.name, self)
+
 
 class Index(Series):
     pass
@@ -1117,4 +1156,71 @@ def repartition(df, divisions):
             return Series(dsk, name, df.name, divisions)
 
 
+class DatetimeAccessor(object):
+    """ Datetime functions
+
+    Examples
+    --------
+
+    >>> df.mydatetime.dt.microsecond  # doctest: +SKIP
+    """
+    def __init__(self, series):
+        self._series = series
+
+    def __dir__(self):
+        return sorted(set(dir(type(self)) + dir(pd.Series.dt)))
+
+    def _property_map(self, key):
+        return self._series.map_partitions(lambda s: getattr(s.dt, key))
+
+    def _function_map(self, key, *args):
+        func = lambda s: getattr(s.dt, key)(*args)
+        return self._series.map_partitions(func, *args)
+
+    def __getattr__(self, key):
+        try:
+            return object.__getattribute__(self, key)
+        except AttributeError:
+            if key in dir(pd.Series.dt):
+                if isinstance(getattr(pd.Series.dt, key), property):
+                    return self._property_map(key)
+                else:
+                    return partial(self._function_map, key)
+            else:
+                raise
+
+
+class StringAccessor(object):
+    """ String functions
+
+    Examples
+    --------
+
+    >>> df.name.lower()  # doctest: +SKIP
+    """
+    def __init__(self, series):
+        self._series = series
+
+    def __dir__(self):
+        return sorted(set(dir(type(self)) + dir(pd.Series.str)))
+
+    def _property_map(self, key):
+        return self._series.map_partitions(lambda s: getattr(s.str, key))
+
+    def _function_map(self, key, *args):
+        func = lambda s: getattr(s.str, key)(*args)
+        return self._series.map_partitions(func, *args)
+
+    def __getattr__(self, key):
+        try:
+            return object.__getattribute__(self, key)
+        except AttributeError:
+            if key in dir(pd.Series.str):
+                if isinstance(getattr(pd.Series.str, key), property):
+                    return self._property_map(key)
+                else:
+                    return partial(self._function_map, key)
+            else:
+                raise
+
 from .shuffle import set_index, set_partition, shuffle


Test patch:
diff --git a/dask/dataframe/tests/test_dataframe.py b/dask/dataframe/tests/test_dataframe.py
--- a/dask/dataframe/tests/test_dataframe.py
+++ b/dask/dataframe/tests/test_dataframe.py
@@ -485,3 +485,47 @@ def test_repartition_on_pandas_dataframe():
     assert isinstance(ddf, dd.Series)
     assert ddf.divisions == (10, 20, 50, 60)
     assert eq(ddf, df.y)
+
+
+def test_embarrassingly_parallel_operations():
+    df = pd.DataFrame({'x': [1, 2, 3, 4, None, 6], 'y': list('abdabd')},
+                      index=[10, 20, 30, 40, 50, 60])
+    a = dd.from_pandas(df, 2)
+
+    assert eq(a.x.astype('float32'), df.x.astype('float32'))
+    assert a.x.astype('float32').compute().dtype == 'float32'
+
+    assert eq(a.x.dropna(), df.x.dropna())
+
+    assert eq(a.x.fillna(100), df.x.fillna(100))
+    assert eq(a.fillna(100), df.fillna(100))
+
+    assert eq(a.x.between(2, 4), df.x.between(2, 4))
+
+    assert eq(a.x.clip(2, 4), df.x.clip(2, 4))
+
+    assert eq(a.x.notnull(), df.x.notnull())
+
+    assert len(a.sample(0.5).compute()) < len(df)
+
+
+def test_datetime_accessor():
+    df = pd.DataFrame({'x': [1, 2, 3, 4]})
+    df['x'] = df.x.astype('M8[us]')
+
+    a = dd.from_pandas(df, 2)
+
+    assert 'date' in dir(a.x.dt)
+
+    assert eq(a.x.dt.date, df.x.dt.date)
+    assert (a.x.dt.to_pydatetime().compute() == df.x.dt.to_pydatetime()).all()
+
+
+def test_str_accessor():
+    df = pd.DataFrame({'x': ['a', 'b', 'c', 'D']})
+
+    a = dd.from_pandas(df, 2)
+
+    assert 'upper' in dir(a.x.str)
+
+    assert eq(a.x.str.upper(), df.x.str.upper())