Repository: pandas-dev/pandas

Issue:
Timestamp.tz_localize() NonExistentTimeError handling
I'm trying to use pandas to speed up timezone conversions on many datetimes and I can't get around `NonExistentTimeErrors`. Pandas `tz_localize()` seems to ignore the `ambiguous` argument for the non existent time case.

Example:

``` python
import pytz
import pandas

tz = pytz.timezone('Europe/Warsaw')
non_existent = datetime.datetime(2015, 3, 29, 2, 30)

tz.normalize(tz.localize(non_existent))
#2015-03-29 03:30:00+02:00

tz.normalize(tz.localize(non_existent, is_dst=False))
#2015-03-29 03:30:00+02:00

pandas.Timestamp(non_existent).tz_localize(tz)
# NonExistentTimeError: 2015-03-29 02:30:00

pandas.Timestamp(non_existent).tz_localize(tz, ambiguous=0)
# NonExistentTimeError: 2015-03-29 02:30:00
```

It would be nice if the `ambiguous` argument worked the same as `is_dst` in pytz. As it is now, it's impossible afaik to reliably localize a series of datetimes using pandas, since any of them might cause a `NonExistentTimeError` and there's no way of guiding pandas what to do with such datetimes.

```
INSTALLED VERSIONS
------------------
commit: None
python: 2.7.5.final.0
python-bits: 64
OS: Linux
OS-release: 3.13.0-39-generic
machine: x86_64
processor: x86_64
byteorder: little
LC_ALL: None
LANG: en_US.UTF-8

pandas: 0.15.1
nose: 1.3.4
Cython: None
numpy: 1.9.1
scipy: None
statsmodels: None
IPython: 2.3.1
sphinx: 1.2.2
patsy: None
dateutil: 2.2
pytz: 2013.9
bottleneck: None
tables: None
numexpr: None
matplotlib: None
openpyxl: None
xlrd: None
xlwt: None
xlsxwriter: None
lxml: None
bs4: None
html5lib: None
httplib2: 0.9
apiclient: 1.3.1
rpy2: None
sqlalchemy: None
pymysql: None
psycopg2: 2.5.4 (dt dec pq3 ext)
```



Discussion:
cc @rockg @ischwabacher 

`ambiguous` is only structured to differentiate duplicate times in the fall transition.  It would be very easy to extend to the spring transition.

@mskrajnowski I have been thinking about this more and wonder if your data is really in a standard time zone and you should be localizing to that and then converting.  For example, what does the data look like for that Spring DST change day?  Are there 24 hours or just 23?  Maybe the localizing issue is masking something else.

It seems to me that the only overlap between the sorts of options that make sense for handling ambiguous times and the options that make sense for nonexistent times are `NaT` and `raise`.  Here are the other possible behaviors I can come up with, though some of them are pretty wacky:
1. Choose the time at the jump.  So `Timestamp('2014-03-09 02:30:00').tz_localize('America/Chicago')` is `Timestamp('2014-03-09 03:00:00-0500', tz='America/Chicago')`.
2. Apply the (non-DST) offset before the discontinuity to the given time, then normalize (in the `pytz` sense).  So `Timestamp('2014-03-09 02:30:00').tz_localize('America/Chicago')` becomes
   `Timestamp('2014-03-09 03:30:00-0500', tz='America/Chicago')`.
3. Apply the (DST) offset after the discontinuity to the given time, then normalize.
4. Apply the "before" offset, then _don't_ normalize.  This yields a nonexistent time, but would allow `date_range` to be emulated by repeated subtraction of an offset from a `Timestamp`.  (So that for instance `Timestamp('2014-03-10 02:30:00-0500', tz='America/Chicago') - Day() - Day()` could equal `Timestamp('2014-03-08 02:30:00-0600', tz='America/Chicago')`.  But this is a lot of crazy complexity for an invariant that I'm starting to think is a bad idea anyway.
5. Apply the "after" offset, then don't normalize.  This is like the previous option but for repeated addition instead of subtraction.

How do these match up against the options for ambiguous time handling?  Does the knob for nonexistent time handling need to be separate from the one for ambiguous times?

@rockg I'm implementing a scheduling application. The user defines working hours in his/her own timezone, then I'm combining the time provided by the user with dates, to get actual work start and end utc datetimes. I can't really forbid the user from setting 02:30 as his work start/end time (maybe he/she likes to wake up in the middle of the night and code ;) ), so I need a way to reliably localize **any** datetime. Even if technically a given time doesn't exist, I still need to output some logical utc datetime.

@ischwabacher I'd go with the way `pytz` handles non existent/ambiguous time.

Unfortunately, `pytz` uses `is_dst=True` to mean option 5, `is_dst=False` to mean option 4, and `is_dst=None` to mean `raise`.  This is partly due to `pytz`'s workaround for limitations in the `datetime` API, which are tentatively scheduled to be fixed in python3.5 (woohoo!), so we will probably see `pytz` switch to behaviors 3 and 2, respectively.

One issue I have here is that `is_dst=True` returns a time that (once normalized) is not DST but would be the given time if it _were_ DST, while `is_dst=False` returns one that is DST but would be the given time if it were _not_ DST.  I am not sure whether this is more or less confusing than swapping them.

Also, if your users set a time of 2:30 as a work start/end time, do you think your users will be least surprised by an alarm at 1:30, 3:00, or 3:30?  Does it depend on whether it's a start or end time?

Since 2:00 becomes 3:00 in that transition, imho it's logical that 2:30 would become 3:30. That's what I get with `pytz` using `localize` and `normalize` (without passing `is_dst`). Of course, now the question is, what to do with a time interval of 2:30 - 3:00, which would become 3:30 - 3:00.
However, I'd still prefer that problem, because now I have better data to work with.

Another thing that would help is if we had an api which would enable us to fix ambiguous and non existent times. Maybe tz_localize could return NaT along with information, why a time wasn't localized? If I had such information, I could, for example , add an hour to non existent times and retry. At the moment tz_localize raises on first error, which isn't very helpful when working with series.

I definitely think there should be an option to return `NaT` instead of raising, but it doesn't seem feasible to attach any other information to that unless you just want a warning, which we should not emit unless it's explicitly requested (since nonexistent times can arise without necessarily coming from a programmer error).

As far as defaults go, I think we should keep `raise` as the default (or possibly `raise` for single operations and `NaT` for vectorized operations if we can do that?) because "errors should not pass silently // unless explicitly silenced". 

Hi guys - Was this released in 0.20.2 yet? I'm seeing the same problem here.
thanks.
This issue is still open, so no, this has not been fixed in 0.20.2. Contributions are welcome.
@jorisvandenbossche @esvhd same problem here. Got some timestamps in central time, and try to localize in EST give me this error. Is there a quick and dirty fix for that? Thanks!

There should be nothing wrong converting to EST as that does not have DST offsets.  Can you post your example?

```
In [10]: ts = pd.Timestamp("2017-03-12 02:30")

In [11]: ts.tz_localize("EST")
Out[11]: Timestamp('2017-03-12 02:30:00-0500', tz='EST')
```
@rockg No I mean I convert from Central time to EST.

`df.timestamp.map(lambda x: x.tz_localize('US/Central', ambiguous = 'NaT').tz_convert('US/Eastern').tz_localize(None))`
@jorisvandenbossche @mskrajnowski @rockg would this be the `pytz`- equivalent (and correct) way to convert from `US/Central` to `US/Eastern` in a Pandas dataframe? 

Assume `df.timestamp` is a naive `pd.to_datetime()` timestamp
 

```
import pytz

tz_est = pytz.timezone('US/Eastern')
tz_central = pytz.timezone('US/Central')

df.timestamp.map(lambda x: tz_central.localize(x).astimezone(tz_est).tz_localize(None)))
```
@jorisvandenbossche @mskrajnowski @rockg any ideas? sorry for the spam but this is an important question in my humble opinion :D

Resolving patch:
diff --git a/pandas/core/arrays/datetimes.py b/pandas/core/arrays/datetimes.py
--- a/pandas/core/arrays/datetimes.py
+++ b/pandas/core/arrays/datetimes.py
@@ -611,7 +611,8 @@ def tz_convert(self, tz):
         # No conversion since timestamps are all UTC to begin with
         return self._shallow_copy(tz=tz)
 
-    def tz_localize(self, tz, ambiguous='raise', errors='raise'):
+    def tz_localize(self, tz, ambiguous='raise', nonexistent='raise',
+                    errors=None):
         """
         Localize tz-naive Datetime Array/Index to tz-aware
         Datetime Array/Index.
@@ -627,8 +628,7 @@ def tz_localize(self, tz, ambiguous='raise', errors='raise'):
         tz : string, pytz.timezone, dateutil.tz.tzfile or None
             Time zone to convert timestamps to. Passing ``None`` will
             remove the time zone information preserving local time.
-        ambiguous : str {'infer', 'NaT', 'raise'} or bool array,
-            default 'raise'
+        ambiguous : 'infer', 'NaT', bool array, default 'raise'
 
             - 'infer' will attempt to infer fall dst-transition hours based on
               order
@@ -639,15 +639,27 @@ def tz_localize(self, tz, ambiguous='raise', errors='raise'):
             - 'raise' will raise an AmbiguousTimeError if there are ambiguous
               times
 
-        errors : {'raise', 'coerce'}, default 'raise'
+        nonexistent : 'shift', 'NaT' default 'raise'
+            A nonexistent time does not exist in a particular timezone
+            where clocks moved forward due to DST.
+
+            - 'shift' will shift the nonexistent times forward to the closest
+              existing time
+            - 'NaT' will return NaT where there are nonexistent times
+            - 'raise' will raise an NonExistentTimeError if there are
+              nonexistent times
+
+            .. versionadded:: 0.24.0
+
+        errors : {'raise', 'coerce'}, default None
 
             - 'raise' will raise a NonExistentTimeError if a timestamp is not
               valid in the specified time zone (e.g. due to a transition from
-              or to DST time)
+              or to DST time). Use ``nonexistent='raise'`` instead.
             - 'coerce' will return NaT if the timestamp can not be converted
-              to the specified time zone
+              to the specified time zone. Use ``nonexistent='NaT'`` instead.
 
-            .. versionadded:: 0.19.0
+            .. deprecated:: 0.24.0
 
         Returns
         -------
@@ -689,6 +701,23 @@ def tz_localize(self, tz, ambiguous='raise', errors='raise'):
                        '2018-03-03 09:00:00'],
                       dtype='datetime64[ns]', freq='D')
         """
+        if errors is not None:
+            warnings.warn("The errors argument is deprecated and will be "
+                          "removed in a future release. Use "
+                          "nonexistent='NaT' or nonexistent='raise' "
+                          "instead.", FutureWarning)
+            if errors == 'coerce':
+                nonexistent = 'NaT'
+            elif errors == 'raise':
+                nonexistent = 'raise'
+            else:
+                raise ValueError("The errors argument must be either 'coerce' "
+                                 "or 'raise'.")
+
+        if nonexistent not in ('raise', 'NaT', 'shift'):
+            raise ValueError("The nonexistent argument must be one of 'raise',"
+                             " 'NaT' or 'shift'")
+
         if self.tz is not None:
             if tz is None:
                 new_dates = conversion.tz_convert(self.asi8, 'UTC', self.tz)
@@ -698,9 +727,9 @@ def tz_localize(self, tz, ambiguous='raise', errors='raise'):
             tz = timezones.maybe_get_tz(tz)
             # Convert to UTC
 
-            new_dates = conversion.tz_localize_to_utc(self.asi8, tz,
-                                                      ambiguous=ambiguous,
-                                                      errors=errors)
+            new_dates = conversion.tz_localize_to_utc(
+                self.asi8, tz, ambiguous=ambiguous, nonexistent=nonexistent,
+            )
         new_dates = new_dates.view(_NS_DTYPE)
         return self._shallow_copy(new_dates, tz=tz)
 
diff --git a/pandas/core/generic.py b/pandas/core/generic.py
--- a/pandas/core/generic.py
+++ b/pandas/core/generic.py
@@ -8627,7 +8627,7 @@ def _tz_convert(ax, tz):
         return result.__finalize__(self)
 
     def tz_localize(self, tz, axis=0, level=None, copy=True,
-                    ambiguous='raise'):
+                    ambiguous='raise', nonexistent='raise'):
         """
         Localize tz-naive TimeSeries to target time zone.
 
@@ -8649,6 +8649,17 @@ def tz_localize(self, tz, axis=0, level=None, copy=True,
             - 'NaT' will return NaT where there are ambiguous times
             - 'raise' will raise an AmbiguousTimeError if there are ambiguous
               times
+        nonexistent : 'shift', 'NaT', default 'raise'
+            A nonexistent time does not exist in a particular timezone
+            where clocks moved forward due to DST.
+
+            - 'shift' will shift the nonexistent times forward to the closest
+              existing time
+            - 'NaT' will return NaT where there are nonexistent times
+            - 'raise' will raise an NonExistentTimeError if there are
+              nonexistent times
+
+            .. versionadded:: 0.24.0
 
         Returns
         -------
@@ -8658,10 +8669,14 @@ def tz_localize(self, tz, axis=0, level=None, copy=True,
         TypeError
             If the TimeSeries is tz-aware and tz is not None.
         """
+        if nonexistent not in ('raise', 'NaT', 'shift'):
+            raise ValueError("The nonexistent argument must be one of 'raise',"
+                             " 'NaT' or 'shift'")
+
         axis = self._get_axis_number(axis)
         ax = self._get_axis(axis)
 
-        def _tz_localize(ax, tz, ambiguous):
+        def _tz_localize(ax, tz, ambiguous, nonexistent):
             if not hasattr(ax, 'tz_localize'):
                 if len(ax) > 0:
                     ax_name = self._get_axis_name(axis)
@@ -8670,19 +8685,23 @@ def _tz_localize(ax, tz, ambiguous):
                 else:
                     ax = DatetimeIndex([], tz=tz)
             else:
-                ax = ax.tz_localize(tz, ambiguous=ambiguous)
+                ax = ax.tz_localize(
+                    tz, ambiguous=ambiguous, nonexistent=nonexistent
+                )
             return ax
 
         # if a level is given it must be a MultiIndex level or
         # equivalent to the axis name
         if isinstance(ax, MultiIndex):
             level = ax._get_level_number(level)
-            new_level = _tz_localize(ax.levels[level], tz, ambiguous)
+            new_level = _tz_localize(
+                ax.levels[level], tz, ambiguous, nonexistent
+            )
             ax = ax.set_levels(new_level, level=level)
         else:
             if level not in (None, 0, ax.name):
                 raise ValueError("The level {0} is not valid".format(level))
-            ax = _tz_localize(ax, tz, ambiguous)
+            ax = _tz_localize(ax, tz, ambiguous, nonexistent)
 
         result = self._constructor(self._data, copy=copy)
         result.set_axis(ax, axis=axis, inplace=True)


Test patch:
diff --git a/pandas/tests/indexes/datetimes/test_timezones.py b/pandas/tests/indexes/datetimes/test_timezones.py
--- a/pandas/tests/indexes/datetimes/test_timezones.py
+++ b/pandas/tests/indexes/datetimes/test_timezones.py
@@ -312,9 +312,13 @@ def test_dti_tz_localize_nonexistent_raise_coerce(self):
             index.tz_localize(tz=tz)
 
         with pytest.raises(pytz.NonExistentTimeError):
-            index.tz_localize(tz=tz, errors='raise')
+            with tm.assert_produces_warning(FutureWarning):
+                index.tz_localize(tz=tz, errors='raise')
 
-        result = index.tz_localize(tz=tz, errors='coerce')
+        with tm.assert_produces_warning(FutureWarning,
+                                        clear=FutureWarning,
+                                        check_stacklevel=False):
+            result = index.tz_localize(tz=tz, errors='coerce')
         test_times = ['2015-03-08 01:00-05:00', 'NaT',
                       '2015-03-08 03:00-04:00']
         dti = to_datetime(test_times, utc=True)
@@ -574,6 +578,42 @@ def test_dti_tz_localize_bdate_range(self):
         localized = dr.tz_localize(pytz.utc)
         tm.assert_index_equal(dr_utc, localized)
 
+    @pytest.mark.parametrize('tz', ['Europe/Warsaw', 'dateutil/Europe/Warsaw'])
+    @pytest.mark.parametrize('method, exp', [
+        ['shift', '2015-03-29 03:00:00'],
+        ['NaT', pd.NaT],
+        ['raise', None],
+        ['foo', 'invalid']
+    ])
+    def test_dti_tz_localize_nonexistent(self, tz, method, exp):
+        # GH 8917
+        n = 60
+        dti = date_range(start='2015-03-29 02:00:00', periods=n, freq='min')
+        if method == 'raise':
+            with pytest.raises(pytz.NonExistentTimeError):
+                dti.tz_localize(tz, nonexistent=method)
+        elif exp == 'invalid':
+            with pytest.raises(ValueError):
+                dti.tz_localize(tz, nonexistent=method)
+        else:
+            result = dti.tz_localize(tz, nonexistent=method)
+            expected = DatetimeIndex([exp] * n, tz=tz)
+            tm.assert_index_equal(result, expected)
+
+    @pytest.mark.filterwarnings('ignore::FutureWarning')
+    def test_dti_tz_localize_errors_deprecation(self):
+        # GH 22644
+        tz = 'Europe/Warsaw'
+        n = 60
+        dti = date_range(start='2015-03-29 02:00:00', periods=n, freq='min')
+        with tm.assert_produces_warning(FutureWarning, check_stacklevel=False):
+            with pytest.raises(ValueError):
+                dti.tz_localize(tz, errors='foo')
+            # make sure errors='coerce' gets mapped correctly to nonexistent
+            result = dti.tz_localize(tz, errors='coerce')
+            expected = dti.tz_localize(tz, nonexistent='NaT')
+            tm.assert_index_equal(result, expected)
+
     # -------------------------------------------------------------
     # DatetimeIndex.normalize
 
diff --git a/pandas/tests/scalar/timestamp/test_timezones.py b/pandas/tests/scalar/timestamp/test_timezones.py
--- a/pandas/tests/scalar/timestamp/test_timezones.py
+++ b/pandas/tests/scalar/timestamp/test_timezones.py
@@ -79,20 +79,44 @@ def test_tz_localize_ambiguous(self):
         ('2015-03-08 02:30', 'US/Pacific'),
         ('2015-03-29 02:00', 'Europe/Paris'),
         ('2015-03-29 02:30', 'Europe/Belgrade')])
+    @pytest.mark.filterwarnings('ignore::FutureWarning')
     def test_tz_localize_nonexistent(self, stamp, tz):
         # GH#13057
         ts = Timestamp(stamp)
         with pytest.raises(NonExistentTimeError):
             ts.tz_localize(tz)
+        # GH 22644
         with pytest.raises(NonExistentTimeError):
-            ts.tz_localize(tz, errors='raise')
-        assert ts.tz_localize(tz, errors='coerce') is NaT
+            with tm.assert_produces_warning(FutureWarning):
+                ts.tz_localize(tz, errors='raise')
+        with tm.assert_produces_warning(FutureWarning):
+            assert ts.tz_localize(tz, errors='coerce') is NaT
 
     def test_tz_localize_errors_ambiguous(self):
         # GH#13057
         ts = Timestamp('2015-11-1 01:00')
         with pytest.raises(AmbiguousTimeError):
-            ts.tz_localize('US/Pacific', errors='coerce')
+            with tm.assert_produces_warning(FutureWarning):
+                ts.tz_localize('US/Pacific', errors='coerce')
+
+    @pytest.mark.filterwarnings('ignore::FutureWarning')
+    def test_tz_localize_errors_invalid_arg(self):
+        # GH 22644
+        tz = 'Europe/Warsaw'
+        ts = Timestamp('2015-03-29 02:00:00')
+        with pytest.raises(ValueError):
+            with tm.assert_produces_warning(FutureWarning):
+                ts.tz_localize(tz, errors='foo')
+
+    def test_tz_localize_errors_coerce(self):
+        # GH 22644
+        # make sure errors='coerce' gets mapped correctly to nonexistent
+        tz = 'Europe/Warsaw'
+        ts = Timestamp('2015-03-29 02:00:00')
+        with tm.assert_produces_warning(FutureWarning):
+            result = ts.tz_localize(tz, errors='coerce')
+        expected = ts.tz_localize(tz, nonexistent='NaT')
+        assert result is expected
 
     @pytest.mark.parametrize('stamp', ['2014-02-01 09:00', '2014-07-08 09:00',
                                        '2014-11-01 17:00', '2014-11-05 00:00'])
@@ -158,6 +182,30 @@ def test_timestamp_tz_localize(self, tz):
         assert result.hour == expected.hour
         assert result == expected
 
+    @pytest.mark.parametrize('tz', ['Europe/Warsaw', 'dateutil/Europe/Warsaw'])
+    def test_timestamp_tz_localize_nonexistent_shift(self, tz):
+        # GH 8917
+        ts = Timestamp('2015-03-29 02:20:00')
+        result = ts.tz_localize(tz, nonexistent='shift')
+        expected = Timestamp('2015-03-29 03:00:00').tz_localize(tz)
+        assert result == expected
+
+    @pytest.mark.parametrize('tz', ['Europe/Warsaw', 'dateutil/Europe/Warsaw'])
+    def test_timestamp_tz_localize_nonexistent_NaT(self, tz):
+        # GH 8917
+        ts = Timestamp('2015-03-29 02:20:00')
+        result = ts.tz_localize(tz, nonexistent='NaT')
+        assert result is NaT
+
+    @pytest.mark.parametrize('tz', ['Europe/Warsaw', 'dateutil/Europe/Warsaw'])
+    def test_timestamp_tz_localize_nonexistent_raise(self, tz):
+        # GH 8917
+        ts = Timestamp('2015-03-29 02:20:00')
+        with pytest.raises(pytz.NonExistentTimeError):
+            ts.tz_localize(tz, nonexistent='raise')
+        with pytest.raises(ValueError):
+            ts.tz_localize(tz, nonexistent='foo')
+
     # ------------------------------------------------------------------
     # Timestamp.tz_convert
 
diff --git a/pandas/tests/series/test_timezones.py b/pandas/tests/series/test_timezones.py
--- a/pandas/tests/series/test_timezones.py
+++ b/pandas/tests/series/test_timezones.py
@@ -13,7 +13,7 @@
 from pandas._libs.tslibs import timezones, conversion
 from pandas.compat import lrange
 from pandas.core.indexes.datetimes import date_range
-from pandas import Series, Timestamp, DatetimeIndex, Index
+from pandas import Series, Timestamp, DatetimeIndex, Index, NaT
 
 
 class TestSeriesTimezones(object):
@@ -33,6 +33,21 @@ def test_series_tz_localize(self):
         tm.assert_raises_regex(TypeError, 'Already tz-aware',
                                ts.tz_localize, 'US/Eastern')
 
+    @pytest.mark.filterwarnings('ignore::FutureWarning')
+    def test_tz_localize_errors_deprecation(self):
+        # GH 22644
+        tz = 'Europe/Warsaw'
+        n = 60
+        rng = date_range(start='2015-03-29 02:00:00', periods=n, freq='min')
+        ts = Series(rng)
+        with tm.assert_produces_warning(FutureWarning, check_stacklevel=False):
+            with pytest.raises(ValueError):
+                ts.dt.tz_localize(tz, errors='foo')
+            # make sure errors='coerce' gets mapped correctly to nonexistent
+            result = ts.dt.tz_localize(tz, errors='coerce')
+            expected = ts.dt.tz_localize(tz, nonexistent='NaT')
+            tm.assert_series_equal(result, expected)
+
     def test_series_tz_localize_ambiguous_bool(self):
         # make sure that we are correctly accepting bool values as ambiguous
 
@@ -60,6 +75,29 @@ def test_series_tz_localize_ambiguous_bool(self):
         result = ser.dt.tz_localize('US/Central', ambiguous=[False])
         tm.assert_series_equal(result, expected1)
 
+    @pytest.mark.parametrize('tz', ['Europe/Warsaw', 'dateutil/Europe/Warsaw'])
+    @pytest.mark.parametrize('method, exp', [
+        ['shift', '2015-03-29 03:00:00'],
+        ['NaT', NaT],
+        ['raise', None],
+        ['foo', 'invalid']
+    ])
+    def test_series_tz_localize_nonexistent(self, tz, method, exp):
+        # GH 8917
+        n = 60
+        dti = date_range(start='2015-03-29 02:00:00', periods=n, freq='min')
+        s = Series(1, dti)
+        if method == 'raise':
+            with pytest.raises(pytz.NonExistentTimeError):
+                s.tz_localize(tz, nonexistent=method)
+        elif exp == 'invalid':
+            with pytest.raises(ValueError):
+                dti.tz_localize(tz, nonexistent=method)
+        else:
+            result = s.tz_localize(tz, nonexistent=method)
+            expected = Series(1, index=DatetimeIndex([exp] * n, tz=tz))
+            tm.assert_series_equal(result, expected)
+
     @pytest.mark.parametrize('tzstr', ['US/Eastern', 'dateutil/US/Eastern'])
     def test_series_tz_localize_empty(self, tzstr):
         # GH#2248