Repository: dask/dask

Issue:
DataFrame loc on string timestamp fails
When we index a timestamp indexed dataframe with a slice of times encoded as strings everything is great

```
result = df.loc['2015-01': '2015-02']
```

However, when we use a single string things fail

```
result = df.loc['2015-01']
```

Probably there is some custom logic in dask.dataframe that is doing actual value comparisons of the input string against the `divisions`.  It would be nice to use whatever logic Pandas uses in order to coerce this input to the proper timestamp.

cc @jreback any pointers?



Discussion:
https://github.com/pydata/pandas/blob/master/pandas/core/indexing.py#L1756 will give you an indexer (or `None`) if its not applicable based on the key and index

Unfortunately we don't have a fully realized index with values to throw this against.

not sure I understand, how is this different that the slicing of two strings case? you don't need a fully materialized index, this can be in a chunk.

I first need to compare these values against the divisions that separate the partitions.  This allows me to select which partitions to send the loc query down to.


Resolving patch:
diff --git a/dask/dataframe/core.py b/dask/dataframe/core.py
--- a/dask/dataframe/core.py
+++ b/dask/dataframe/core.py
@@ -1,8 +1,6 @@
 from __future__ import absolute_import, division, print_function
 
-import bisect
 from collections import Iterator
-from datetime import datetime
 from distutils.version import LooseVersion
 import math
 import operator
@@ -30,6 +28,8 @@
                      pseudorandom, derived_from, different_seeds, funcname)
 from ..base import Base, compute, tokenize, normalize_token
 from ..async import get_sync
+from .indexing import (_partition_of_index_value, _loc, _try_loc,
+                       _coerce_loc_index, _maybe_partial_time_string)
 from .utils import nonempty_sample_df
 
 no_default = '__no_default__'
@@ -161,7 +161,7 @@ class _Frame(Base):
 
     def __new__(cls, dsk, _name, metadata, divisions):
         if (np.isscalar(metadata) or metadata is None or
-            isinstance(metadata, (Series, pd.Series))):
+           isinstance(metadata, (Series, pd.Series))):
             return Series(dsk, _name, metadata, divisions)
         else:
             return DataFrame(dsk, _name, metadata, divisions)
@@ -391,12 +391,13 @@ def _loc(self, ind):
         if isinstance(ind, Series):
             return self._loc_series(ind)
         if self.known_divisions:
+            ind = _maybe_partial_time_string(self._pd.index, ind, kind='loc')
             if isinstance(ind, slice):
                 return self._loc_slice(ind)
             else:
                 return self._loc_element(ind)
         else:
-            return map_partitions(try_loc, self, self, ind)
+            return map_partitions(_try_loc, self, self, ind)
 
     def _loc_series(self, ind):
         if not self.divisions == ind.divisions:
@@ -416,6 +417,7 @@ def _loc_element(self, ind):
     def _loc_slice(self, ind):
         name = 'loc-%s' % tokenize(ind, self)
         assert ind.step in (None, 1)
+
         if ind.start:
             start = _partition_of_index_value(self.divisions, ind.start)
         else:
@@ -1393,12 +1395,19 @@ def columns(self, columns):
     def __getitem__(self, key):
         name = 'getitem-%s' % tokenize(self, key)
         if np.isscalar(key):
+
+            if isinstance(self._pd.index, (pd.DatetimeIndex, pd.PeriodIndex)):
+                if key not in self._pd.columns:
+                    return self._loc(key)
+
             # error is raised from pandas
             dummy = self._pd[_extract_pd(key)]
             dsk = dict(((name, i), (operator.getitem, (self._name, i), key))
-                        for i in range(self.npartitions))
+                       for i in range(self.npartitions))
             return self._constructor_sliced(merge(self.dask, dsk), name,
                                             dummy, self.divisions)
+        elif isinstance(key, slice):
+            return self._loc(key)
 
         if isinstance(key, list):
             # error is raised from pandas
@@ -1859,67 +1868,6 @@ def _assign(df, *pairs):
     return df.assign(**kwargs)
 
 
-def _partition_of_index_value(divisions, val):
-    """ In which partition does this value lie?
-
-    >>> _partition_of_index_value([0, 5, 10], 3)
-    0
-    >>> _partition_of_index_value([0, 5, 10], 8)
-    1
-    >>> _partition_of_index_value([0, 5, 10], 100)
-    1
-    >>> _partition_of_index_value([0, 5, 10], 5)  # left-inclusive divisions
-    1
-    """
-    if divisions[0] is None:
-        raise ValueError(
-            "Can not use loc on DataFrame without known divisions")
-    val = _coerce_loc_index(divisions, val)
-    i = bisect.bisect_right(divisions, val)
-    return min(len(divisions) - 2, max(0, i - 1))
-
-
-def _loc(df, start, stop, include_right_boundary=True):
-    """
-
-    >>> df = pd.DataFrame({'x': [10, 20, 30, 40, 50]}, index=[1, 2, 2, 3, 4])
-    >>> _loc(df, 2, None)
-        x
-    2  20
-    2  30
-    3  40
-    4  50
-    >>> _loc(df, 1, 3)
-        x
-    1  10
-    2  20
-    2  30
-    3  40
-    >>> _loc(df, 1, 3, include_right_boundary=False)
-        x
-    1  10
-    2  20
-    2  30
-    """
-    result = df.loc[start:stop]
-    if not include_right_boundary:
-        right_index = result.index.get_slice_bound(stop, 'left', 'loc')
-        result = result.iloc[:right_index]
-    return result
-
-
-def _coerce_loc_index(divisions, o):
-    """ Transform values to be comparable against divisions
-
-    This is particularly valuable to use with pandas datetimes
-    """
-    if divisions and isinstance(divisions[0], datetime):
-        return pd.Timestamp(o)
-    if divisions and isinstance(divisions[0], np.datetime64):
-        return np.datetime64(o).astype(divisions[0].dtype)
-    return o
-
-
 def elemwise(op, *args, **kwargs):
     """ Elementwise operation for dask.Dataframes """
     columns = kwargs.pop('columns', no_default)
@@ -1938,7 +1886,6 @@ def elemwise(op, *args, **kwargs):
     other = [(i, arg) for i, arg in enumerate(args)
              if not isinstance(arg, (_Frame, Scalar))]
 
-
     # adjust the key length of Scalar
     keys = [d._keys() *  n if isinstance(d, Scalar)
             else d._keys() for d in dasks]
@@ -2012,7 +1959,7 @@ def reduction(x, chunk, aggregate, token=None):
 
     b = '{0}--aggregation-{1}'.format(token, token_key)
     dsk2 = {(b, 0): (aggregate, (remove_empties,
-                     [(a,i) for i in range(x.npartitions)]))}
+                     [(a, i) for i in range(x.npartitions)]))}
 
     return Scalar(merge(x.dask, dsk, dsk2), b)
 
@@ -2076,7 +2023,7 @@ def apply_concat_apply(args, chunk=None, aggregate=None,
                for i in range(args[0].npartitions))
 
     b = '{0}--second-{1}'.format(token, token_key)
-    conc = (_concat,(list, [(a, i) for i in range(args[0].npartitions)]))
+    conc = (_concat, (list, [(a, i) for i in range(args[0].npartitions)]))
     if not aggregate_kwargs:
         dsk2 = {(b, 0): (aggregate, conc)}
     else:
@@ -2732,7 +2679,6 @@ def call(obj, attr, *args):
         return getattr(obj.dt, attr)(*args)
 
 
-
 class StringAccessor(Accessor):
     """ Accessor object for string properties of the Series values.
 
@@ -2752,13 +2698,6 @@ def call(obj, attr, *args):
         return getattr(obj.str, attr)(*args)
 
 
-def try_loc(df, ind):
-    try:
-        return df.loc[ind]
-    except KeyError:
-        return df.head(0)
-
-
 def set_sorted_index(df, index, drop=True, **kwargs):
     if not isinstance(index, Series):
         index2 = df[index]
diff --git a/dask/dataframe/indexing.py b/dask/dataframe/indexing.py
new file mode 100644
--- /dev/null
+++ b/dask/dataframe/indexing.py
@@ -0,0 +1,106 @@
+from __future__ import absolute_import, division, print_function
+
+from datetime import datetime
+
+import bisect
+import numpy as np
+import pandas as pd
+
+
+def _partition_of_index_value(divisions, val):
+    """ In which partition does this value lie?
+
+    >>> _partition_of_index_value([0, 5, 10], 3)
+    0
+    >>> _partition_of_index_value([0, 5, 10], 8)
+    1
+    >>> _partition_of_index_value([0, 5, 10], 100)
+    1
+    >>> _partition_of_index_value([0, 5, 10], 5)  # left-inclusive divisions
+    1
+    """
+    if divisions[0] is None:
+        raise ValueError(
+            "Can not use loc on DataFrame without known divisions")
+    val = _coerce_loc_index(divisions, val)
+    i = bisect.bisect_right(divisions, val)
+    return min(len(divisions) - 2, max(0, i - 1))
+
+
+def _loc(df, start, stop, include_right_boundary=True):
+    """
+
+    >>> df = pd.DataFrame({'x': [10, 20, 30, 40, 50]}, index=[1, 2, 2, 3, 4])
+    >>> _loc(df, 2, None)
+        x
+    2  20
+    2  30
+    3  40
+    4  50
+    >>> _loc(df, 1, 3)
+        x
+    1  10
+    2  20
+    2  30
+    3  40
+    >>> _loc(df, 1, 3, include_right_boundary=False)
+        x
+    1  10
+    2  20
+    2  30
+    """
+    result = df.loc[start:stop]
+    if not include_right_boundary:
+        right_index = result.index.get_slice_bound(stop, 'left', 'loc')
+        result = result.iloc[:right_index]
+    return result
+
+
+def _try_loc(df, ind):
+    try:
+        return df.loc[ind]
+    except KeyError:
+        return df.head(0)
+
+
+def _coerce_loc_index(divisions, o):
+    """ Transform values to be comparable against divisions
+
+    This is particularly valuable to use with pandas datetimes
+    """
+    if divisions and isinstance(divisions[0], datetime):
+        return pd.Timestamp(o)
+    if divisions and isinstance(divisions[0], np.datetime64):
+        return np.datetime64(o).astype(divisions[0].dtype)
+    return o
+
+
+def _maybe_partial_time_string(index, indexer, kind):
+    """
+    Convert indexer for partial string selection
+    if data has DatetimeIndex/PeriodIndex
+    """
+
+    # do not pass dd.Index
+    assert isinstance(index, pd.Index)
+
+    if not isinstance(index, (pd.DatetimeIndex, pd.PeriodIndex)):
+        return indexer
+
+    if isinstance(indexer, slice):
+        if isinstance(indexer.start, pd.compat.string_types):
+            start = index._maybe_cast_slice_bound(indexer.start, 'left', kind)
+        else:
+            start = indexer.start
+
+        if isinstance(indexer.stop, pd.compat.string_types):
+            stop = index._maybe_cast_slice_bound(indexer.stop, 'right', kind)
+        else:
+            stop = indexer.stop
+        return slice(start, stop)
+
+    elif isinstance(indexer, pd.compat.string_types):
+        start = index._maybe_cast_slice_bound(indexer, 'left', 'loc')
+        stop = index._maybe_cast_slice_bound(indexer, 'right', 'loc')
+        return slice(start, stop)
+    return indexer


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
@@ -654,94 +654,6 @@ def test_index():
     assert eq(d.index, full.index)
 
 
-def test_loc():
-    assert d.loc[3:8].divisions[0] == 3
-    assert d.loc[3:8].divisions[-1] == 8
-
-    assert d.loc[5].divisions == (5, 5)
-
-    assert eq(d.loc[5], full.loc[5:5])
-    assert eq(d.loc[3:8], full.loc[3:8])
-    assert eq(d.loc[:8], full.loc[:8])
-    assert eq(d.loc[3:], full.loc[3:])
-
-    assert eq(d.a.loc[5], full.a.loc[5:5])
-    assert eq(d.a.loc[3:8], full.a.loc[3:8])
-    assert eq(d.a.loc[:8], full.a.loc[:8])
-    assert eq(d.a.loc[3:], full.a.loc[3:])
-
-    assert raises(KeyError, lambda: d.loc[1000])
-    assert eq(d.loc[1000:], full.loc[1000:])
-    assert eq(d.loc[-2000:-1000], full.loc[-2000:-1000])
-
-    assert sorted(d.loc[5].dask) == sorted(d.loc[5].dask)
-    assert sorted(d.loc[5].dask) != sorted(d.loc[6].dask)
-
-
-def test_loc_non_informative_index():
-    df = pd.DataFrame({'x': [1, 2, 3, 4]}, index=[10, 20, 30, 40])
-    ddf = dd.from_pandas(df, npartitions=2, sort=True)
-    ddf.divisions = (None,) * 3
-    assert not ddf.known_divisions
-
-    ddf.loc[20:30].compute(get=dask.get)
-
-    assert eq(ddf.loc[20:30], df.loc[20:30])
-
-    df = pd.DataFrame({'x': [1, 2, 3, 4]}, index=[10, 20, 20, 40])
-    ddf = dd.from_pandas(df, npartitions=2, sort=True)
-    assert eq(ddf.loc[20], df.loc[20:20])
-
-
-def test_loc_with_text_dates():
-    A = tm.makeTimeSeries(10).iloc[:5]
-    B = tm.makeTimeSeries(10).iloc[5:]
-    s = dd.Series({('df', 0): A, ('df', 1): B}, 'df', None,
-                  [A.index.min(), B.index.min(), B.index.max()])
-
-    assert s.loc['2000': '2010'].divisions == s.divisions
-    assert eq(s.loc['2000': '2010'], s)
-    assert len(s.loc['2000-01-03': '2000-01-05'].compute()) == 3
-
-
-def test_loc_with_series():
-    assert eq(d.loc[d.a % 2 == 0], full.loc[full.a % 2 == 0])
-
-    assert sorted(d.loc[d.a % 2].dask) == sorted(d.loc[d.a % 2].dask)
-    assert sorted(d.loc[d.a % 2].dask) != sorted(d.loc[d.a % 3].dask)
-
-
-def test_getitem():
-    df = pd.DataFrame({'A': [1, 2, 3, 4, 5, 6, 7, 8, 9],
-                       'B': [9, 8, 7, 6, 5, 4, 3, 2, 1],
-                       'C': [True, False, True] * 3},
-                      columns=list('ABC'))
-    ddf = dd.from_pandas(df, 2)
-    assert eq(ddf['A'], df['A'])
-    tm.assert_series_equal(ddf['A']._pd, ddf._pd['A'])# check cache consistency
-
-    assert eq(ddf[['A', 'B']], df[['A', 'B']])
-    tm.assert_frame_equal(ddf[['A', 'B']]._pd, ddf._pd[['A', 'B']])
-
-    assert eq(ddf[ddf.C], df[df.C])
-    tm.assert_series_equal(ddf.C._pd, ddf._pd.C)
-
-    assert eq(ddf[ddf.C.repartition([0, 2, 5, 8])], df[df.C])
-
-    assert raises(KeyError, lambda: df['X'])
-    assert raises(KeyError, lambda: df[['A', 'X']])
-    assert raises(AttributeError, lambda: df.X)
-
-    # not str/unicode
-    df = pd.DataFrame(np.random.randn(10, 5))
-    ddf = dd.from_pandas(df, 2)
-    assert eq(ddf[0], df[0])
-    assert eq(ddf[[1, 2]], df[[1, 2]])
-
-    assert raises(KeyError, lambda: df[8])
-    assert raises(KeyError, lambda: df[[1, 8]])
-
-
 def test_assign():
     assert eq(d.assign(c=d.a + 1, e=d.a + d.b),
               full.assign(c=full.a + 1, e=full.a + full.b))
@@ -1306,29 +1218,6 @@ def test_empty_max():
     assert eq(a.x.max(), 1)
 
 
-def test_loc_on_numpy_datetimes():
-    df = pd.DataFrame({'x': [1, 2, 3]},
-                      index=list(map(np.datetime64, ['2014', '2015', '2016'])))
-    a = dd.from_pandas(df, 2)
-    a.divisions = list(map(np.datetime64, a.divisions))
-
-    assert eq(a.loc['2014': '2015'], a.loc['2014': '2015'])
-
-
-def test_loc_on_pandas_datetimes():
-    df = pd.DataFrame({'x': [1, 2, 3]},
-                      index=list(map(pd.Timestamp, ['2014', '2015', '2016'])))
-    a = dd.from_pandas(df, 2)
-    a.divisions = list(map(pd.Timestamp, a.divisions))
-
-    assert eq(a.loc['2014': '2015'], a.loc['2014': '2015'])
-
-
-def test_coerce_loc_index():
-    for t in [pd.Timestamp, np.datetime64]:
-        assert isinstance(_coerce_loc_index([t('2014')], '2014'), t)
-
-
 def test_nlargest_series():
     s = pd.Series([1, 3, 5, 2, 4, 6])
     ss = dd.from_pandas(s, npartitions=2)
diff --git a/dask/dataframe/tests/test_indexing.py b/dask/dataframe/tests/test_indexing.py
new file mode 100644
--- /dev/null
+++ b/dask/dataframe/tests/test_indexing.py
@@ -0,0 +1,241 @@
+
+import pandas as pd
+import pandas.util.testing as tm
+import numpy as np
+
+import dask
+from dask.utils import raises
+import dask.dataframe as dd
+
+from dask.dataframe.core import _coerce_loc_index
+from dask.dataframe.utils import eq
+
+
+dsk = {('x', 0): pd.DataFrame({'a': [1, 2, 3], 'b': [4, 5, 6]},
+                              index=[0, 1, 3]),
+       ('x', 1): pd.DataFrame({'a': [4, 5, 6], 'b': [3, 2, 1]},
+                              index=[5, 6, 8]),
+       ('x', 2): pd.DataFrame({'a': [7, 8, 9], 'b': [0, 0, 0]},
+                              index=[9, 9, 9])}
+d = dd.DataFrame(dsk, 'x', ['a', 'b'], [0, 5, 9, 9])
+full = d.compute()
+
+
+def test_loc():
+    assert d.loc[3:8].divisions[0] == 3
+    assert d.loc[3:8].divisions[-1] == 8
+
+    assert d.loc[5].divisions == (5, 5)
+
+    assert eq(d.loc[5], full.loc[5:5])
+    assert eq(d.loc[3:8], full.loc[3:8])
+    assert eq(d.loc[:8], full.loc[:8])
+    assert eq(d.loc[3:], full.loc[3:])
+
+    assert eq(d.a.loc[5], full.a.loc[5:5])
+    assert eq(d.a.loc[3:8], full.a.loc[3:8])
+    assert eq(d.a.loc[:8], full.a.loc[:8])
+    assert eq(d.a.loc[3:], full.a.loc[3:])
+
+    assert raises(KeyError, lambda: d.loc[1000])
+    assert eq(d.loc[1000:], full.loc[1000:])
+    assert eq(d.loc[-2000:-1000], full.loc[-2000:-1000])
+
+    assert sorted(d.loc[5].dask) == sorted(d.loc[5].dask)
+    assert sorted(d.loc[5].dask) != sorted(d.loc[6].dask)
+
+
+def test_loc_non_informative_index():
+    df = pd.DataFrame({'x': [1, 2, 3, 4]}, index=[10, 20, 30, 40])
+    ddf = dd.from_pandas(df, npartitions=2, sort=True)
+    ddf.divisions = (None,) * 3
+    assert not ddf.known_divisions
+
+    ddf.loc[20:30].compute(get=dask.get)
+
+    assert eq(ddf.loc[20:30], df.loc[20:30])
+
+    df = pd.DataFrame({'x': [1, 2, 3, 4]}, index=[10, 20, 20, 40])
+    ddf = dd.from_pandas(df, npartitions=2, sort=True)
+    assert eq(ddf.loc[20], df.loc[20:20])
+
+
+def test_loc_with_text_dates():
+    A = tm.makeTimeSeries(10).iloc[:5]
+    B = tm.makeTimeSeries(10).iloc[5:]
+    s = dd.Series({('df', 0): A, ('df', 1): B}, 'df', None,
+                  [A.index.min(), B.index.min(), B.index.max()])
+
+    assert s.loc['2000': '2010'].divisions == s.divisions
+    assert eq(s.loc['2000': '2010'], s)
+    assert len(s.loc['2000-01-03': '2000-01-05'].compute()) == 3
+
+
+def test_loc_with_series():
+    assert eq(d.loc[d.a % 2 == 0], full.loc[full.a % 2 == 0])
+
+    assert sorted(d.loc[d.a % 2].dask) == sorted(d.loc[d.a % 2].dask)
+    assert sorted(d.loc[d.a % 2].dask) != sorted(d.loc[d.a % 3].dask)
+
+
+def test_getitem():
+    df = pd.DataFrame({'A': [1, 2, 3, 4, 5, 6, 7, 8, 9],
+                       'B': [9, 8, 7, 6, 5, 4, 3, 2, 1],
+                       'C': [True, False, True] * 3},
+                      columns=list('ABC'))
+    ddf = dd.from_pandas(df, 2)
+    assert eq(ddf['A'], df['A'])
+    # check cache consistency
+    tm.assert_series_equal(ddf['A']._pd, ddf._pd['A'])
+
+    assert eq(ddf[['A', 'B']], df[['A', 'B']])
+    tm.assert_frame_equal(ddf[['A', 'B']]._pd, ddf._pd[['A', 'B']])
+
+    assert eq(ddf[ddf.C], df[df.C])
+    tm.assert_series_equal(ddf.C._pd, ddf._pd.C)
+
+    assert eq(ddf[ddf.C.repartition([0, 2, 5, 8])], df[df.C])
+
+    assert raises(KeyError, lambda: df['X'])
+    assert raises(KeyError, lambda: df[['A', 'X']])
+    assert raises(AttributeError, lambda: df.X)
+
+    # not str/unicode
+    df = pd.DataFrame(np.random.randn(10, 5))
+    ddf = dd.from_pandas(df, 2)
+    assert eq(ddf[0], df[0])
+    assert eq(ddf[[1, 2]], df[[1, 2]])
+
+    assert raises(KeyError, lambda: df[8])
+    assert raises(KeyError, lambda: df[[1, 8]])
+
+
+def test_getitem_slice():
+    df = pd.DataFrame({'A': [1, 2, 3, 4, 5, 6, 7, 8, 9],
+                       'B': [9, 8, 7, 6, 5, 4, 3, 2, 1],
+                       'C': [True, False, True] * 3},
+                      index=list('abcdefghi'))
+    ddf = dd.from_pandas(df, 3)
+    assert eq(ddf['a':'e'], df['a':'e'])
+    assert eq(ddf['a':'b'], df['a':'b'])
+    assert eq(ddf['f':], df['f':])
+
+
+def test_loc_on_numpy_datetimes():
+    df = pd.DataFrame({'x': [1, 2, 3]},
+                      index=list(map(np.datetime64, ['2014', '2015', '2016'])))
+    a = dd.from_pandas(df, 2)
+    a.divisions = list(map(np.datetime64, a.divisions))
+
+    assert eq(a.loc['2014': '2015'], a.loc['2014': '2015'])
+
+
+def test_loc_on_pandas_datetimes():
+    df = pd.DataFrame({'x': [1, 2, 3]},
+                      index=list(map(pd.Timestamp, ['2014', '2015', '2016'])))
+    a = dd.from_pandas(df, 2)
+    a.divisions = list(map(pd.Timestamp, a.divisions))
+
+    assert eq(a.loc['2014': '2015'], a.loc['2014': '2015'])
+
+
+def test_coerce_loc_index():
+    for t in [pd.Timestamp, np.datetime64]:
+        assert isinstance(_coerce_loc_index([t('2014')], '2014'), t)
+
+
+def test_loc_timestamp_str():
+
+    df = pd.DataFrame({'A': np.random.randn(100), 'B': np.random.randn(100)},
+                      index=pd.date_range('2011-01-01', freq='H', periods=100))
+    ddf = dd.from_pandas(df, 10)
+
+    # partial string slice
+    assert eq(df.loc['2011-01-02'],
+              ddf.loc['2011-01-02'])
+    assert eq(df.loc['2011-01-02':'2011-01-10'],
+              ddf.loc['2011-01-02':'2011-01-10'])
+    # same reso, dask result is always DataFrame
+    assert eq(df.loc['2011-01-02 10:00'].to_frame().T,
+              ddf.loc['2011-01-02 10:00'])
+
+    # series
+    assert eq(df.A.loc['2011-01-02'],
+              ddf.A.loc['2011-01-02'])
+    assert eq(df.A.loc['2011-01-02':'2011-01-10'],
+              ddf.A.loc['2011-01-02':'2011-01-10'])
+
+    # slice with timestamp (dask result must be DataFrame)
+    assert eq(df.loc[pd.Timestamp('2011-01-02')].to_frame().T,
+              ddf.loc[pd.Timestamp('2011-01-02')])
+    assert eq(df.loc[pd.Timestamp('2011-01-02'):pd.Timestamp('2011-01-10')],
+              ddf.loc[pd.Timestamp('2011-01-02'):pd.Timestamp('2011-01-10')])
+    assert eq(df.loc[pd.Timestamp('2011-01-02 10:00')].to_frame().T,
+              ddf.loc[pd.Timestamp('2011-01-02 10:00')])
+
+    df = pd.DataFrame({'A': np.random.randn(100), 'B': np.random.randn(100)},
+                      index=pd.date_range('2011-01-01', freq='M', periods=100))
+    ddf = dd.from_pandas(df, 50)
+    assert eq(df.loc['2011-01'], ddf.loc['2011-01'])
+    assert eq(df.loc['2011'], ddf.loc['2011'])
+
+    assert eq(df.loc['2011-01':'2012-05'], ddf.loc['2011-01':'2012-05'])
+    assert eq(df.loc['2011':'2015'], ddf.loc['2011':'2015'])
+
+    # series
+    assert eq(df.B.loc['2011-01'], ddf.B.loc['2011-01'])
+    assert eq(df.B.loc['2011'], ddf.B.loc['2011'])
+
+    assert eq(df.B.loc['2011-01':'2012-05'], ddf.B.loc['2011-01':'2012-05'])
+    assert eq(df.B.loc['2011':'2015'], ddf.B.loc['2011':'2015'])
+
+
+def test_getitem_timestamp_str():
+
+    df = pd.DataFrame({'A': np.random.randn(100), 'B': np.random.randn(100)},
+                      index=pd.date_range('2011-01-01', freq='H', periods=100))
+    ddf = dd.from_pandas(df, 10)
+
+    # partial string slice
+    assert eq(df['2011-01-02'],
+              ddf['2011-01-02'])
+    assert eq(df['2011-01-02':'2011-01-10'],
+              df['2011-01-02':'2011-01-10'])
+
+    df = pd.DataFrame({'A': np.random.randn(100), 'B': np.random.randn(100)},
+                      index=pd.date_range('2011-01-01', freq='D', periods=100))
+    ddf = dd.from_pandas(df, 50)
+    assert eq(df['2011-01'], ddf['2011-01'])
+    assert eq(df['2011'], ddf['2011'])
+
+    assert eq(df['2011-01':'2012-05'], ddf['2011-01':'2012-05'])
+    assert eq(df['2011':'2015'], ddf['2011':'2015'])
+
+
+def test_loc_period_str():
+    # .loc with PeriodIndex doesn't support partial string indexing
+    # https://github.com/pydata/pandas/issues/13429
+    pass
+
+
+def test_getitem_period_str():
+
+    df = pd.DataFrame({'A': np.random.randn(100), 'B': np.random.randn(100)},
+                      index=pd.period_range('2011-01-01', freq='H', periods=100))
+    ddf = dd.from_pandas(df, 10)
+
+    # partial string slice
+    assert eq(df['2011-01-02'],
+              ddf['2011-01-02'])
+    assert eq(df['2011-01-02':'2011-01-10'],
+              df['2011-01-02':'2011-01-10'])
+    # same reso, dask result is always DataFrame
+
+    df = pd.DataFrame({'A': np.random.randn(100), 'B': np.random.randn(100)},
+                      index=pd.period_range('2011-01-01', freq='D', periods=100))
+    ddf = dd.from_pandas(df, 50)
+    assert eq(df['2011-01'], ddf['2011-01'])
+    assert eq(df['2011'], ddf['2011'])
+
+    assert eq(df['2011-01':'2012-05'], ddf['2011-01':'2012-05'])
+    assert eq(df['2011':'2015'], ddf['2011':'2015'])