Skip to content

ENH/BUG: Period/PeriodIndex supports NaT #7485

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 19, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion doc/source/v0.14.1.txt
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ Enhancements

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


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


.. _whatsnew_0141.performance:
Expand Down Expand Up @@ -239,6 +239,9 @@ Bug Fixes


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



- BUG in ``resample`` raises ``ValueError`` when target contains ``NaT`` (:issue:`7227`)
Expand Down
2 changes: 1 addition & 1 deletion pandas/tseries/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -809,7 +809,7 @@ def to_period(self, freq=None):
if freq is None:
freq = get_period_alias(self.freqstr)

return PeriodIndex(self.values, freq=freq, tz=self.tz)
return PeriodIndex(self.values, name=self.name, freq=freq, tz=self.tz)

def order(self, return_indexer=False, ascending=True):
"""
Expand Down
86 changes: 67 additions & 19 deletions pandas/tseries/period.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,12 @@ def __init__(self, value=None, freq=None, ordinal=None,
converted = other.asfreq(freq)
self.ordinal = converted.ordinal

elif com._is_null_datelike_scalar(value) or value in tslib._nat_strings:
self.ordinal = tslib.iNaT
if freq is None:
raise ValueError("If value is NaT, freq cannot be None "
"because it cannot be inferred")

elif isinstance(value, compat.string_types) or com.is_integer(value):
if com.is_integer(value):
value = str(value)
Expand Down Expand Up @@ -136,6 +142,8 @@ def __eq__(self, other):
if isinstance(other, Period):
if other.freq != self.freq:
raise ValueError("Cannot compare non-conforming periods")
if self.ordinal == tslib.iNaT or other.ordinal == tslib.iNaT:
return False
return (self.ordinal == other.ordinal
and _gfc(self.freq) == _gfc(other.freq))
return NotImplemented
Expand All @@ -148,26 +156,38 @@ def __hash__(self):

def __add__(self, other):
if com.is_integer(other):
return Period(ordinal=self.ordinal + other, freq=self.freq)
if self.ordinal == tslib.iNaT:
ordinal = self.ordinal
else:
ordinal = self.ordinal + other
return Period(ordinal=ordinal, freq=self.freq)
else: # pragma: no cover
raise TypeError(other)
return NotImplemented
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this tested somewhere?


def __sub__(self, other):
if com.is_integer(other):
return Period(ordinal=self.ordinal - other, freq=self.freq)
if self.ordinal == tslib.iNaT:
ordinal = self.ordinal
else:
ordinal = self.ordinal - other
return Period(ordinal=ordinal, freq=self.freq)
if isinstance(other, Period):
if other.freq != self.freq:
raise ValueError("Cannot do arithmetic with "
"non-conforming periods")
if self.ordinal == tslib.iNaT or other.ordinal == tslib.iNaT:
return Period(ordinal=tslib.iNaT, freq=self.freq)
return self.ordinal - other.ordinal
else: # pragma: no cover
raise TypeError(other)
return NotImplemented

def _comp_method(func, name):
def f(self, other):
if isinstance(other, Period):
if other.freq != self.freq:
raise ValueError("Cannot compare non-conforming periods")
if self.ordinal == tslib.iNaT or other.ordinal == tslib.iNaT:
return False
return func(self.ordinal, other.ordinal)
else:
raise TypeError(other)
Expand Down Expand Up @@ -213,7 +233,10 @@ def start_time(self):

@property
def end_time(self):
ordinal = (self + 1).start_time.value - 1
if self.ordinal == tslib.iNaT:
ordinal = self.ordinal
else:
ordinal = (self + 1).start_time.value - 1
return Timestamp(ordinal)

def to_timestamp(self, freq=None, how='start', tz=None):
Expand Down Expand Up @@ -480,6 +503,11 @@ def _period_index_cmp(opname):
Wrap comparison operations to convert datetime-like to datetime64
"""
def wrapper(self, other):
if opname == '__ne__':
fill_value = True
else:
fill_value = False

if isinstance(other, Period):
func = getattr(self.values, opname)
if other.freq != self.freq:
Expand All @@ -489,12 +517,26 @@ def wrapper(self, other):
elif isinstance(other, PeriodIndex):
if other.freq != self.freq:
raise AssertionError("Frequencies must be equal")
return getattr(self.values, opname)(other.values)

result = getattr(self.values, opname)(other.values)

mask = (com.mask_missing(self.values, tslib.iNaT) |
com.mask_missing(other.values, tslib.iNaT))
if mask.any():
result[mask] = fill_value

return result
else:
other = Period(other, freq=self.freq)
func = getattr(self.values, opname)
result = func(other.ordinal)

if other.ordinal == tslib.iNaT:
result.fill(fill_value)
mask = self.values == tslib.iNaT
if mask.any():
result[mask] = fill_value

return result
return wrapper

Expand Down Expand Up @@ -712,7 +754,7 @@ def asof_locs(self, where, mask):

@property
def asobject(self):
return Index(self._box_values(self.values), dtype=object)
return Index(self._box_values(self.values), name=self.name, dtype=object)

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

end = how == 'E'
new_data = tslib.period_asfreq_arr(self.values, base1, base2, end)

result = new_data.view(PeriodIndex)
result.name = self.name
result.freq = freq
return result
return self._simple_new(new_data, self.name, freq=freq)

def to_datetime(self, dayfirst=False):
return self.to_timestamp()
Expand Down Expand Up @@ -868,16 +906,23 @@ def shift(self, n):
-------
shifted : PeriodIndex
"""
if n == 0:
return self

return PeriodIndex(data=self.values + n, freq=self.freq)
mask = self.values == tslib.iNaT
values = self.values + n
values[mask] = tslib.iNaT
return PeriodIndex(data=values, name=self.name, freq=self.freq)

def __add__(self, other):
return PeriodIndex(ordinal=self.values + other, freq=self.freq)
try:
return self.shift(other)
except TypeError:
# self.values + other raises TypeError for invalid input
return NotImplemented

def __sub__(self, other):
return PeriodIndex(ordinal=self.values - other, freq=self.freq)
try:
return self.shift(-other)
except TypeError:
return NotImplemented

@property
def inferred_type(self):
Expand Down Expand Up @@ -1207,8 +1252,11 @@ def _get_ordinal_range(start, end, periods, freq):
is_start_per = isinstance(start, Period)
is_end_per = isinstance(end, Period)

if is_start_per and is_end_per and (start.freq != end.freq):
if is_start_per and is_end_per and start.freq != end.freq:
raise ValueError('Start and end must have same freq')
if ((is_start_per and start.ordinal == tslib.iNaT) or
(is_end_per and end.ordinal == tslib.iNaT)):
raise ValueError('Start and end must not be NaT')

if freq is None:
if is_start_per:
Expand Down
Loading