Repository: pandas-dev/pandas

Issue:
AmbiguousTimeError in floor() operation
This is probably related to #18885 

Given a DataFrame with a DST change on it:

```python
df1=pd.DataFrame([pd.to_datetime('2017-10-29 02:00:00+02:00'),
                  pd.to_datetime('2017-10-29 02:00:00+01:00'),
                  pd.to_datetime('2017-10-29 03:00:00+01:00')],columns=['date'])
df1['date'] = df1['date'].dt.tz_localize('UTC').dt.tz_convert('Europe/Madrid')
df1['value'] = 1
```

When we try to do a `floor()` or `ceil()` operation, we get an AmbiguousTimeError exception:

```python
df1.date.dt.floor('H')
```

### Expected output

```
0   2017-10-29 02:00:00+02:00
1   2017-10-29 02:00:00+01:00
2   2017-10-29 03:00:00+01:00
```

### Actual output

```
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "(...)/venv/lib/python3.6/site-packages/pandas/core/accessor.py", line 115, in f
    return self._delegate_method(name, *args, **kwargs)
  File "(...)/venv/lib/python3.6/site-packages/pandas/core/indexes/accessors.py", line 131, in _delegate_method
    result = method(*args, **kwargs)
  File "(...)/venv/lib/python3.6/site-packages/pandas/core/indexes/datetimelike.py", line 118, in floor
    return self._round(freq, np.floor)
  File "(...)/venv/lib/python3.6/site-packages/pandas/core/indexes/datetimelike.py", line 110, in _round
    self._shallow_copy(result, **attribs))
  File "(...)/venv/lib/python3.6/site-packages/pandas/core/indexes/datetimelike.py", line 230, in _ensure_localized
    result = result.tz_localize(self.tz)
  File "(...)/venv/lib/python3.6/site-packages/pandas/util/_decorators.py", line 118, in wrapper
    return func(*args, **kwargs)
  File "(...)/venv/lib/python3.6/site-packages/pandas/core/indexes/datetimes.py", line 1858, in tz_localize
    errors=errors)
  File "pandas/_libs/tslib.pyx", line 3593, in pandas._libs.tslib.tz_localize_to_utc
pytz.exceptions.AmbiguousTimeError: Cannot infer dst time from Timestamp('2017-10-29 02:00:00'), try using the 'ambiguous' argument
```


Discussion:
yes there a number of issues related to this. the fix is all the same.

Resolving patch:
diff --git a/pandas/core/indexes/datetimelike.py b/pandas/core/indexes/datetimelike.py
--- a/pandas/core/indexes/datetimelike.py
+++ b/pandas/core/indexes/datetimelike.py
@@ -99,6 +99,18 @@ class TimelikeOps(object):
             frequency like 'S' (second) not 'ME' (month end). See
             :ref:`frequency aliases <timeseries.offset_aliases>` for
             a list of possible `freq` values.
+        ambiguous : 'infer', bool-ndarray, 'NaT', default 'raise'
+            - 'infer' will attempt to infer fall dst-transition hours based on
+              order
+            - bool-ndarray where True signifies a DST time, False designates
+              a non-DST time (note that this flag is only applicable for
+              ambiguous times)
+            - 'NaT' will return NaT where there are ambiguous times
+            - 'raise' will raise an AmbiguousTimeError if there are ambiguous
+              times
+            Only relevant for DatetimeIndex
+
+            .. versionadded:: 0.24.0
 
         Returns
         -------
@@ -168,7 +180,7 @@ class TimelikeOps(object):
         """
     )
 
-    def _round(self, freq, rounder):
+    def _round(self, freq, rounder, ambiguous):
         # round the local times
         values = _ensure_datetimelike_to_i8(self)
         result = round_ns(values, rounder, freq)
@@ -180,19 +192,20 @@ def _round(self, freq, rounder):
         if 'tz' in attribs:
             attribs['tz'] = None
         return self._ensure_localized(
-            self._shallow_copy(result, **attribs))
+            self._shallow_copy(result, **attribs), ambiguous
+        )
 
     @Appender((_round_doc + _round_example).format(op="round"))
-    def round(self, freq, *args, **kwargs):
-        return self._round(freq, np.round)
+    def round(self, freq, ambiguous='raise'):
+        return self._round(freq, np.round, ambiguous)
 
     @Appender((_round_doc + _floor_example).format(op="floor"))
-    def floor(self, freq):
-        return self._round(freq, np.floor)
+    def floor(self, freq, ambiguous='raise'):
+        return self._round(freq, np.floor, ambiguous)
 
     @Appender((_round_doc + _ceil_example).format(op="ceil"))
-    def ceil(self, freq):
-        return self._round(freq, np.ceil)
+    def ceil(self, freq, ambiguous='raise'):
+        return self._round(freq, np.ceil, ambiguous)
 
 
 class DatetimeIndexOpsMixin(DatetimeLikeArrayMixin):
@@ -264,7 +277,7 @@ def _evaluate_compare(self, other, op):
         except TypeError:
             return result
 
-    def _ensure_localized(self, result):
+    def _ensure_localized(self, result, ambiguous='raise'):
         """
         ensure that we are re-localized
 
@@ -274,6 +287,8 @@ def _ensure_localized(self, result):
         Parameters
         ----------
         result : DatetimeIndex / i8 ndarray
+        ambiguous : str, bool, or bool-ndarray
+            default 'raise'
 
         Returns
         -------
@@ -284,7 +299,7 @@ def _ensure_localized(self, result):
         if getattr(self, 'tz', None) is not None:
             if not isinstance(result, ABCIndexClass):
                 result = self._simple_new(result)
-            result = result.tz_localize(self.tz)
+            result = result.tz_localize(self.tz, ambiguous=ambiguous)
         return result
 
     def _box_values_as_index(self):


Test patch:
diff --git a/pandas/tests/scalar/timestamp/test_unary_ops.py b/pandas/tests/scalar/timestamp/test_unary_ops.py
--- a/pandas/tests/scalar/timestamp/test_unary_ops.py
+++ b/pandas/tests/scalar/timestamp/test_unary_ops.py
@@ -132,6 +132,28 @@ def test_floor(self):
         expected = Timestamp('20130101')
         assert result == expected
 
+    @pytest.mark.parametrize('method', ['ceil', 'round', 'floor'])
+    def test_round_dst_border(self, method):
+        # GH 18946 round near DST
+        ts = Timestamp('2017-10-29 00:00:00', tz='UTC').tz_convert(
+            'Europe/Madrid'
+        )
+        #
+        result = getattr(ts, method)('H', ambiguous=True)
+        assert result == ts
+
+        result = getattr(ts, method)('H', ambiguous=False)
+        expected = Timestamp('2017-10-29 01:00:00', tz='UTC').tz_convert(
+            'Europe/Madrid'
+        )
+        assert result == expected
+
+        result = getattr(ts, method)('H', ambiguous='NaT')
+        assert result is NaT
+
+        with pytest.raises(pytz.AmbiguousTimeError):
+            getattr(ts, method)('H', ambiguous='raise')
+
     # --------------------------------------------------------------
     # Timestamp.replace
 
diff --git a/pandas/tests/series/test_datetime_values.py b/pandas/tests/series/test_datetime_values.py
--- a/pandas/tests/series/test_datetime_values.py
+++ b/pandas/tests/series/test_datetime_values.py
@@ -5,6 +5,7 @@
 import calendar
 import unicodedata
 import pytest
+import pytz
 
 from datetime import datetime, time, date
 
@@ -95,42 +96,6 @@ def compare(s, name):
             expected = Series(exp_values, index=s.index, name='xxx')
             tm.assert_series_equal(result, expected)
 
-        # round
-        s = Series(pd.to_datetime(['2012-01-01 13:00:00',
-                                   '2012-01-01 12:01:00',
-                                   '2012-01-01 08:00:00']), name='xxx')
-        result = s.dt.round('D')
-        expected = Series(pd.to_datetime(['2012-01-02', '2012-01-02',
-                                          '2012-01-01']), name='xxx')
-        tm.assert_series_equal(result, expected)
-
-        # round with tz
-        result = (s.dt.tz_localize('UTC')
-                   .dt.tz_convert('US/Eastern')
-                   .dt.round('D'))
-        exp_values = pd.to_datetime(['2012-01-01', '2012-01-01',
-                                     '2012-01-01']).tz_localize('US/Eastern')
-        expected = Series(exp_values, name='xxx')
-        tm.assert_series_equal(result, expected)
-
-        # floor
-        s = Series(pd.to_datetime(['2012-01-01 13:00:00',
-                                   '2012-01-01 12:01:00',
-                                   '2012-01-01 08:00:00']), name='xxx')
-        result = s.dt.floor('D')
-        expected = Series(pd.to_datetime(['2012-01-01', '2012-01-01',
-                                          '2012-01-01']), name='xxx')
-        tm.assert_series_equal(result, expected)
-
-        # ceil
-        s = Series(pd.to_datetime(['2012-01-01 13:00:00',
-                                   '2012-01-01 12:01:00',
-                                   '2012-01-01 08:00:00']), name='xxx')
-        result = s.dt.ceil('D')
-        expected = Series(pd.to_datetime(['2012-01-02', '2012-01-02',
-                                          '2012-01-02']), name='xxx')
-        tm.assert_series_equal(result, expected)
-
         # datetimeindex with tz
         s = Series(date_range('20130101', periods=5, tz='US/Eastern'),
                    name='xxx')
@@ -261,6 +226,64 @@ def get_dir(s):
             with pytest.raises(com.SettingWithCopyError):
                 s.dt.hour[0] = 5
 
+    @pytest.mark.parametrize('method, dates', [
+        ['round', ['2012-01-02', '2012-01-02', '2012-01-01']],
+        ['floor', ['2012-01-01', '2012-01-01', '2012-01-01']],
+        ['ceil', ['2012-01-02', '2012-01-02', '2012-01-02']]
+    ])
+    def test_dt_round(self, method, dates):
+        # round
+        s = Series(pd.to_datetime(['2012-01-01 13:00:00',
+                                   '2012-01-01 12:01:00',
+                                   '2012-01-01 08:00:00']), name='xxx')
+        result = getattr(s.dt, method)('D')
+        expected = Series(pd.to_datetime(dates), name='xxx')
+        tm.assert_series_equal(result, expected)
+
+    def test_dt_round_tz(self):
+        s = Series(pd.to_datetime(['2012-01-01 13:00:00',
+                                   '2012-01-01 12:01:00',
+                                   '2012-01-01 08:00:00']), name='xxx')
+        result = (s.dt.tz_localize('UTC')
+                  .dt.tz_convert('US/Eastern')
+                  .dt.round('D'))
+
+        exp_values = pd.to_datetime(['2012-01-01', '2012-01-01',
+                                     '2012-01-01']).tz_localize('US/Eastern')
+        expected = Series(exp_values, name='xxx')
+        tm.assert_series_equal(result, expected)
+
+    @pytest.mark.parametrize('method', ['ceil', 'round', 'floor'])
+    def test_dt_round_tz_ambiguous(self, method):
+        # GH 18946 round near DST
+        df1 = pd.DataFrame([
+            pd.to_datetime('2017-10-29 02:00:00+02:00', utc=True),
+            pd.to_datetime('2017-10-29 02:00:00+01:00', utc=True),
+            pd.to_datetime('2017-10-29 03:00:00+01:00', utc=True)
+        ],
+            columns=['date'])
+        df1['date'] = df1['date'].dt.tz_convert('Europe/Madrid')
+        # infer
+        result = getattr(df1.date.dt, method)('H', ambiguous='infer')
+        expected = df1['date']
+        tm.assert_series_equal(result, expected)
+
+        # bool-array
+        result = getattr(df1.date.dt, method)(
+            'H', ambiguous=[True, False, False]
+        )
+        tm.assert_series_equal(result, expected)
+
+        # NaT
+        result = getattr(df1.date.dt, method)('H', ambiguous='NaT')
+        expected = df1['date'].copy()
+        expected.iloc[0:2] = pd.NaT
+        tm.assert_series_equal(result, expected)
+
+        # raise
+        with pytest.raises(pytz.AmbiguousTimeError):
+            getattr(df1.date.dt, method)('H', ambiguous='raise')
+
     def test_dt_namespace_accessor_categorical(self):
         # GH 19468
         dti = DatetimeIndex(['20171111', '20181212']).repeat(2)