Skip to content

Commit 4950474

Browse files
committed
Merge pull request #7915 from mtrbean/ENH-7846
ENH: New `level` argument for DataFrame.tz_localize and DataFrame.tz_convert (GH7846)
2 parents c2ebf4c + 6f51919 commit 4950474

File tree

4 files changed

+174
-17
lines changed

4 files changed

+174
-17
lines changed

doc/source/v0.15.0.txt

+3
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,9 @@ previously results in ``Exception`` or ``TypeError`` (:issue:`7812`)
162162
didx
163163
didx.tz_localize(None)
164164

165+
- ``DataFrame.tz_localize`` and ``DataFrame.tz_convert`` now accepts an optional ``level`` argument
166+
for localizing a specific level of a MultiIndex (:issue:`7846`)
167+
165168
.. _whatsnew_0150.refactoring:
166169

167170
Internal Refactoring

pandas/core/generic.py

+50-16
Original file line numberDiff line numberDiff line change
@@ -3467,14 +3467,18 @@ def truncate(self, before=None, after=None, axis=None, copy=True):
34673467

34683468
return result
34693469

3470-
def tz_convert(self, tz, axis=0, copy=True):
3470+
def tz_convert(self, tz, axis=0, level=None, copy=True):
34713471
"""
34723472
Convert the axis to target time zone. If it is time zone naive, it
34733473
will be localized to the passed time zone.
34743474
34753475
Parameters
34763476
----------
34773477
tz : string or pytz.timezone object
3478+
axis : the axis to convert
3479+
level : int, str, default None
3480+
If axis ia a MultiIndex, convert a specific level. Otherwise
3481+
must be None
34783482
copy : boolean, default True
34793483
Also make a copy of the underlying data
34803484
@@ -3484,27 +3488,44 @@ def tz_convert(self, tz, axis=0, copy=True):
34843488
axis = self._get_axis_number(axis)
34853489
ax = self._get_axis(axis)
34863490

3487-
if not hasattr(ax, 'tz_convert'):
3488-
if len(ax) > 0:
3489-
ax_name = self._get_axis_name(axis)
3490-
raise TypeError('%s is not a valid DatetimeIndex or PeriodIndex' %
3491-
ax_name)
3491+
def _tz_convert(ax, tz):
3492+
if not hasattr(ax, 'tz_convert'):
3493+
if len(ax) > 0:
3494+
ax_name = self._get_axis_name(axis)
3495+
raise TypeError('%s is not a valid DatetimeIndex or PeriodIndex' %
3496+
ax_name)
3497+
else:
3498+
ax = DatetimeIndex([],tz=tz)
34923499
else:
3493-
ax = DatetimeIndex([],tz=tz)
3500+
ax = ax.tz_convert(tz)
3501+
return ax
3502+
3503+
# if a level is given it must be a MultiIndex level or
3504+
# equivalent to the axis name
3505+
if isinstance(ax, MultiIndex):
3506+
level = ax._get_level_number(level)
3507+
new_level = _tz_convert(ax.levels[level], tz)
3508+
ax = ax.set_levels(new_level, level=level)
34943509
else:
3495-
ax = ax.tz_convert(tz)
3510+
if level not in (None, 0, ax.name):
3511+
raise ValueError("The level {0} is not valid".format(level))
3512+
ax = _tz_convert(ax, tz)
34963513

34973514
result = self._constructor(self._data, copy=copy)
34983515
result.set_axis(axis,ax)
34993516
return result.__finalize__(self)
35003517

3501-
def tz_localize(self, tz, axis=0, copy=True, infer_dst=False):
3518+
def tz_localize(self, tz, axis=0, level=None, copy=True, infer_dst=False):
35023519
"""
35033520
Localize tz-naive TimeSeries to target time zone
35043521
35053522
Parameters
35063523
----------
35073524
tz : string or pytz.timezone object
3525+
axis : the axis to localize
3526+
level : int, str, default None
3527+
If axis ia a MultiIndex, localize a specific level. Otherwise
3528+
must be None
35083529
copy : boolean, default True
35093530
Also make a copy of the underlying data
35103531
infer_dst : boolean, default False
@@ -3516,15 +3537,28 @@ def tz_localize(self, tz, axis=0, copy=True, infer_dst=False):
35163537
axis = self._get_axis_number(axis)
35173538
ax = self._get_axis(axis)
35183539

3519-
if not hasattr(ax, 'tz_localize'):
3520-
if len(ax) > 0:
3521-
ax_name = self._get_axis_name(axis)
3522-
raise TypeError('%s is not a valid DatetimeIndex or PeriodIndex' %
3523-
ax_name)
3540+
def _tz_localize(ax, tz, infer_dst):
3541+
if not hasattr(ax, 'tz_localize'):
3542+
if len(ax) > 0:
3543+
ax_name = self._get_axis_name(axis)
3544+
raise TypeError('%s is not a valid DatetimeIndex or PeriodIndex' %
3545+
ax_name)
3546+
else:
3547+
ax = DatetimeIndex([],tz=tz)
35243548
else:
3525-
ax = DatetimeIndex([],tz=tz)
3549+
ax = ax.tz_localize(tz, infer_dst=infer_dst)
3550+
return ax
3551+
3552+
# if a level is given it must be a MultiIndex level or
3553+
# equivalent to the axis name
3554+
if isinstance(ax, MultiIndex):
3555+
level = ax._get_level_number(level)
3556+
new_level = _tz_localize(ax.levels[level], tz, infer_dst)
3557+
ax = ax.set_levels(new_level, level=level)
35263558
else:
3527-
ax = ax.tz_localize(tz, infer_dst=infer_dst)
3559+
if level not in (None, 0, ax.name):
3560+
raise ValueError("The level {0} is not valid".format(level))
3561+
ax = _tz_localize(ax, tz, infer_dst)
35283562

35293563
result = self._constructor(self._data, copy=copy)
35303564
result.set_axis(axis,ax)

pandas/tests/test_generic.py

+75-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import pandas as pd
88

99
from pandas import (Index, Series, DataFrame, Panel,
10-
isnull, notnull,date_range)
10+
isnull, notnull, date_range, period_range)
1111
from pandas.core.index import Index, MultiIndex
1212

1313
import pandas.core.common as com
@@ -1102,6 +1102,80 @@ def finalize(self, other, method=None, **kwargs):
11021102
DataFrame._metadata = _metadata
11031103
DataFrame.__finalize__ = _finalize
11041104

1105+
def test_tz_convert_and_localize(self):
1106+
l0 = date_range('20140701', periods=5, freq='D')
1107+
1108+
# TODO: l1 should be a PeriodIndex for testing
1109+
# after GH2106 is addressed
1110+
with tm.assertRaises(NotImplementedError):
1111+
period_range('20140701', periods=1).tz_convert('UTC')
1112+
with tm.assertRaises(NotImplementedError):
1113+
period_range('20140701', periods=1).tz_localize('UTC')
1114+
# l1 = period_range('20140701', periods=5, freq='D')
1115+
l1 = date_range('20140701', periods=5, freq='D')
1116+
1117+
int_idx = Index(range(5))
1118+
1119+
for fn in ['tz_localize', 'tz_convert']:
1120+
1121+
if fn == 'tz_convert':
1122+
l0 = l0.tz_localize('UTC')
1123+
l1 = l1.tz_localize('UTC')
1124+
1125+
for idx in [l0, l1]:
1126+
1127+
l0_expected = getattr(idx, fn)('US/Pacific')
1128+
l1_expected = getattr(idx, fn)('US/Pacific')
1129+
1130+
df1 = DataFrame(np.ones(5), index=l0)
1131+
df1 = getattr(df1, fn)('US/Pacific')
1132+
self.assertTrue(df1.index.equals(l0_expected))
1133+
1134+
# MultiIndex
1135+
# GH7846
1136+
df2 = DataFrame(np.ones(5),
1137+
MultiIndex.from_arrays([l0, l1]))
1138+
1139+
df3 = getattr(df2, fn)('US/Pacific', level=0)
1140+
self.assertFalse(df3.index.levels[0].equals(l0))
1141+
self.assertTrue(df3.index.levels[0].equals(l0_expected))
1142+
self.assertTrue(df3.index.levels[1].equals(l1))
1143+
self.assertFalse(df3.index.levels[1].equals(l1_expected))
1144+
1145+
df3 = getattr(df2, fn)('US/Pacific', level=1)
1146+
self.assertTrue(df3.index.levels[0].equals(l0))
1147+
self.assertFalse(df3.index.levels[0].equals(l0_expected))
1148+
self.assertTrue(df3.index.levels[1].equals(l1_expected))
1149+
self.assertFalse(df3.index.levels[1].equals(l1))
1150+
1151+
df4 = DataFrame(np.ones(5),
1152+
MultiIndex.from_arrays([int_idx, l0]))
1153+
1154+
df5 = getattr(df4, fn)('US/Pacific', level=1)
1155+
self.assertTrue(df3.index.levels[0].equals(l0))
1156+
self.assertFalse(df3.index.levels[0].equals(l0_expected))
1157+
self.assertTrue(df3.index.levels[1].equals(l1_expected))
1158+
self.assertFalse(df3.index.levels[1].equals(l1))
1159+
1160+
# Bad Inputs
1161+
for fn in ['tz_localize', 'tz_convert']:
1162+
# Not DatetimeIndex / PeriodIndex
1163+
with tm.assertRaisesRegexp(TypeError, 'DatetimeIndex'):
1164+
df = DataFrame(index=int_idx)
1165+
df = getattr(df, fn)('US/Pacific')
1166+
1167+
# Not DatetimeIndex / PeriodIndex
1168+
with tm.assertRaisesRegexp(TypeError, 'DatetimeIndex'):
1169+
df = DataFrame(np.ones(5),
1170+
MultiIndex.from_arrays([int_idx, l0]))
1171+
df = getattr(df, fn)('US/Pacific', level=0)
1172+
1173+
# Invalid level
1174+
with tm.assertRaisesRegexp(ValueError, 'not valid'):
1175+
df = DataFrame(index=l0)
1176+
df = getattr(df, fn)('US/Pacific', level=1)
1177+
1178+
11051179
class TestPanel(tm.TestCase, Generic):
11061180
_typ = Panel
11071181
_comparator = lambda self, x, y: assert_panel_equal(x, y)

pandas/tseries/period.py

+46
Original file line numberDiff line numberDiff line change
@@ -1174,6 +1174,52 @@ def __setstate__(self, state):
11741174
else:
11751175
raise Exception("invalid pickle state")
11761176
_unpickle_compat = __setstate__
1177+
1178+
def tz_convert(self, tz):
1179+
"""
1180+
Convert tz-aware DatetimeIndex from one time zone to another (using pytz/dateutil)
1181+
1182+
Parameters
1183+
----------
1184+
tz : string, pytz.timezone, dateutil.tz.tzfile or None
1185+
Time zone for time. Corresponding timestamps would be converted to
1186+
time zone of the TimeSeries.
1187+
None will remove timezone holding UTC time.
1188+
1189+
Returns
1190+
-------
1191+
normalized : DatetimeIndex
1192+
1193+
Note
1194+
----
1195+
Not currently implemented for PeriodIndex
1196+
"""
1197+
raise NotImplementedError("Not yet implemented for PeriodIndex")
1198+
1199+
def tz_localize(self, tz, infer_dst=False):
1200+
"""
1201+
Localize tz-naive DatetimeIndex to given time zone (using pytz/dateutil),
1202+
or remove timezone from tz-aware DatetimeIndex
1203+
1204+
Parameters
1205+
----------
1206+
tz : string, pytz.timezone, dateutil.tz.tzfile or None
1207+
Time zone for time. Corresponding timestamps would be converted to
1208+
time zone of the TimeSeries.
1209+
None will remove timezone holding local time.
1210+
infer_dst : boolean, default False
1211+
Attempt to infer fall dst-transition hours based on order
1212+
1213+
Returns
1214+
-------
1215+
localized : DatetimeIndex
1216+
1217+
Note
1218+
----
1219+
Not currently implemented for PeriodIndex
1220+
"""
1221+
raise NotImplementedError("Not yet implemented for PeriodIndex")
1222+
11771223
PeriodIndex._add_numeric_methods_disabled()
11781224

11791225
def _get_ordinal_range(start, end, periods, freq):

0 commit comments

Comments
 (0)