Skip to content

Commit 6602007

Browse files
committed
ENH: Period supports NaT
1 parent 741b2fa commit 6602007

File tree

5 files changed

+282
-32
lines changed

5 files changed

+282
-32
lines changed

doc/source/v0.14.1.txt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ Enhancements
130130

131131
- All offsets ``apply``, ``rollforward`` and ``rollback`` can now handle ``np.datetime64``, previously results in ``ApplyTypeError`` (:issue:`7452`)
132132

133-
133+
- ``Period`` and ``PeriodIndex`` can contain ``NaT`` in its values (:issue:`7485`)
134134

135135

136136
.. _whatsnew_0141.performance:
@@ -239,6 +239,9 @@ Bug Fixes
239239

240240

241241
- Bug in passing input with ``tzinfo`` to some offsets ``apply``, ``rollforward`` or ``rollback`` resets ``tzinfo`` or raises ``ValueError`` (:issue:`7465`)
242+
- Bug in ``DatetimeIndex.to_period``, ``PeriodIndex.asobject``, ``PeriodIndex.to_timestamp`` doesn't preserve ``name`` (:issue:`7485`)
243+
- Bug in ``DatetimeIndex.to_period`` and ``PeriodIndex.to_timestanp`` handle ``NaT`` incorrectly (:issue:`7228`)
244+
242245

243246

244247
- BUG in ``resample`` raises ``ValueError`` when target contains ``NaT`` (:issue:`7227`)

pandas/tseries/index.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -809,7 +809,7 @@ def to_period(self, freq=None):
809809
if freq is None:
810810
freq = get_period_alias(self.freqstr)
811811

812-
return PeriodIndex(self.values, freq=freq, tz=self.tz)
812+
return PeriodIndex(self.values, name=self.name, freq=freq, tz=self.tz)
813813

814814
def order(self, return_indexer=False, ascending=True):
815815
"""

pandas/tseries/period.py

Lines changed: 67 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,12 @@ def __init__(self, value=None, freq=None, ordinal=None,
102102
converted = other.asfreq(freq)
103103
self.ordinal = converted.ordinal
104104

105+
elif com._is_null_datelike_scalar(value) or value in tslib._nat_strings:
106+
self.ordinal = tslib.iNaT
107+
if freq is None:
108+
raise ValueError("If value is NaT, freq cannot be None "
109+
"because it cannot be inferred")
110+
105111
elif isinstance(value, compat.string_types) or com.is_integer(value):
106112
if com.is_integer(value):
107113
value = str(value)
@@ -136,6 +142,8 @@ def __eq__(self, other):
136142
if isinstance(other, Period):
137143
if other.freq != self.freq:
138144
raise ValueError("Cannot compare non-conforming periods")
145+
if self.ordinal == tslib.iNaT or other.ordinal == tslib.iNaT:
146+
return False
139147
return (self.ordinal == other.ordinal
140148
and _gfc(self.freq) == _gfc(other.freq))
141149
return NotImplemented
@@ -148,26 +156,38 @@ def __hash__(self):
148156

149157
def __add__(self, other):
150158
if com.is_integer(other):
151-
return Period(ordinal=self.ordinal + other, freq=self.freq)
159+
if self.ordinal == tslib.iNaT:
160+
ordinal = self.ordinal
161+
else:
162+
ordinal = self.ordinal + other
163+
return Period(ordinal=ordinal, freq=self.freq)
152164
else: # pragma: no cover
153-
raise TypeError(other)
165+
return NotImplemented
154166

155167
def __sub__(self, other):
156168
if com.is_integer(other):
157-
return Period(ordinal=self.ordinal - other, freq=self.freq)
169+
if self.ordinal == tslib.iNaT:
170+
ordinal = self.ordinal
171+
else:
172+
ordinal = self.ordinal - other
173+
return Period(ordinal=ordinal, freq=self.freq)
158174
if isinstance(other, Period):
159175
if other.freq != self.freq:
160176
raise ValueError("Cannot do arithmetic with "
161177
"non-conforming periods")
178+
if self.ordinal == tslib.iNaT or other.ordinal == tslib.iNaT:
179+
return Period(ordinal=tslib.iNaT, freq=self.freq)
162180
return self.ordinal - other.ordinal
163181
else: # pragma: no cover
164-
raise TypeError(other)
182+
return NotImplemented
165183

166184
def _comp_method(func, name):
167185
def f(self, other):
168186
if isinstance(other, Period):
169187
if other.freq != self.freq:
170188
raise ValueError("Cannot compare non-conforming periods")
189+
if self.ordinal == tslib.iNaT or other.ordinal == tslib.iNaT:
190+
return False
171191
return func(self.ordinal, other.ordinal)
172192
else:
173193
raise TypeError(other)
@@ -213,7 +233,10 @@ def start_time(self):
213233

214234
@property
215235
def end_time(self):
216-
ordinal = (self + 1).start_time.value - 1
236+
if self.ordinal == tslib.iNaT:
237+
ordinal = self.ordinal
238+
else:
239+
ordinal = (self + 1).start_time.value - 1
217240
return Timestamp(ordinal)
218241

219242
def to_timestamp(self, freq=None, how='start', tz=None):
@@ -480,6 +503,11 @@ def _period_index_cmp(opname):
480503
Wrap comparison operations to convert datetime-like to datetime64
481504
"""
482505
def wrapper(self, other):
506+
if opname == '__ne__':
507+
fill_value = True
508+
else:
509+
fill_value = False
510+
483511
if isinstance(other, Period):
484512
func = getattr(self.values, opname)
485513
if other.freq != self.freq:
@@ -489,12 +517,26 @@ def wrapper(self, other):
489517
elif isinstance(other, PeriodIndex):
490518
if other.freq != self.freq:
491519
raise AssertionError("Frequencies must be equal")
492-
return getattr(self.values, opname)(other.values)
520+
521+
result = getattr(self.values, opname)(other.values)
522+
523+
mask = (com.mask_missing(self.values, tslib.iNaT) |
524+
com.mask_missing(other.values, tslib.iNaT))
525+
if mask.any():
526+
result[mask] = fill_value
527+
528+
return result
493529
else:
494530
other = Period(other, freq=self.freq)
495531
func = getattr(self.values, opname)
496532
result = func(other.ordinal)
497533

534+
if other.ordinal == tslib.iNaT:
535+
result.fill(fill_value)
536+
mask = self.values == tslib.iNaT
537+
if mask.any():
538+
result[mask] = fill_value
539+
498540
return result
499541
return wrapper
500542

@@ -712,7 +754,7 @@ def asof_locs(self, where, mask):
712754

713755
@property
714756
def asobject(self):
715-
return Index(self._box_values(self.values), dtype=object)
757+
return Index(self._box_values(self.values), name=self.name, dtype=object)
716758

717759
def _array_values(self):
718760
return self.asobject
@@ -768,11 +810,7 @@ def asfreq(self, freq=None, how='E'):
768810

769811
end = how == 'E'
770812
new_data = tslib.period_asfreq_arr(self.values, base1, base2, end)
771-
772-
result = new_data.view(PeriodIndex)
773-
result.name = self.name
774-
result.freq = freq
775-
return result
813+
return self._simple_new(new_data, self.name, freq=freq)
776814

777815
def to_datetime(self, dayfirst=False):
778816
return self.to_timestamp()
@@ -868,16 +906,23 @@ def shift(self, n):
868906
-------
869907
shifted : PeriodIndex
870908
"""
871-
if n == 0:
872-
return self
873-
874-
return PeriodIndex(data=self.values + n, freq=self.freq)
909+
mask = self.values == tslib.iNaT
910+
values = self.values + n
911+
values[mask] = tslib.iNaT
912+
return PeriodIndex(data=values, name=self.name, freq=self.freq)
875913

876914
def __add__(self, other):
877-
return PeriodIndex(ordinal=self.values + other, freq=self.freq)
915+
try:
916+
return self.shift(other)
917+
except TypeError:
918+
# self.values + other raises TypeError for invalid input
919+
return NotImplemented
878920

879921
def __sub__(self, other):
880-
return PeriodIndex(ordinal=self.values - other, freq=self.freq)
922+
try:
923+
return self.shift(-other)
924+
except TypeError:
925+
return NotImplemented
881926

882927
@property
883928
def inferred_type(self):
@@ -1207,8 +1252,11 @@ def _get_ordinal_range(start, end, periods, freq):
12071252
is_start_per = isinstance(start, Period)
12081253
is_end_per = isinstance(end, Period)
12091254

1210-
if is_start_per and is_end_per and (start.freq != end.freq):
1255+
if is_start_per and is_end_per and start.freq != end.freq:
12111256
raise ValueError('Start and end must have same freq')
1257+
if ((is_start_per and start.ordinal == tslib.iNaT) or
1258+
(is_end_per and end.ordinal == tslib.iNaT)):
1259+
raise ValueError('Start and end must not be NaT')
12121260

12131261
if freq is None:
12141262
if is_start_per:

0 commit comments

Comments
 (0)