Skip to content

ENH: Add isnat function and make comparison tests NAT specific #8421

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 4 commits into from
May 7, 2017
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
16 changes: 16 additions & 0 deletions doc/release/1.13.0-notes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,11 @@ In an N-dimensional array, the user can now choose the axis along which to look
for duplicate N-1-dimensional elements using ``numpy.unique``. The original
behaviour is recovered if ``axis=None`` (default).

``np.isnat`` function to test for NaT special datetime and timedelta values
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
``np.isnat`` can now be used to find the positions of special NaT values
within datetime and timedelta arrays. This is analogous to ``np.isnan``.

``isin`` function, improving on ``in1d``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The new function ``isin`` tests whether each element of an N-dimensonal
Expand Down Expand Up @@ -460,6 +465,17 @@ is not the expected behavior both according to documentation and intuitively.
Now, -inf < x < inf is considered ``True`` for any real number x and all
other cases fail.

``assert_array_`` and masked arrays ``assert_equal`` hide less warnings
-----------------------------------------------------------------------
Some warnings that were previously hidden by the ``assert_array_``
functions are not hidden anymore. In most cases the warnings should be
correct and, should they occur, will require changes to the tests using
these functions.
For the masked array ``assert_equal`` version, warnings may occur when
comparing NaT. The function presently does not handle NaT or NaN
specifically and it may be best to avoid it at this time should a warning
show up due to this change.

``offset`` attribute value in ``memmap`` objects
------------------------------------------------
The ``offset`` attribute in a ``memmap`` object is now set to the
Expand Down
6 changes: 6 additions & 0 deletions numpy/core/code_generators/generate_umath.py
Original file line number Diff line number Diff line change
Expand Up @@ -799,6 +799,12 @@ def english_upper(s):
None,
TD(inexact, out='?'),
),
'isnat':
Ufunc(1, 1, None,
docstrings.get('numpy.core.umath.isnat'),
'PyUFunc_IsNaTTypeResolver',
TD(times, out='?'),
),
'isinf':
Ufunc(1, 1, None,
docstrings.get('numpy.core.umath.isinf'),
Expand Down
37 changes: 36 additions & 1 deletion numpy/core/code_generators/ufunc_docstrings.py
Original file line number Diff line number Diff line change
Expand Up @@ -1690,7 +1690,7 @@ def add_newdoc(place, name, doc):

See Also
--------
isinf, isneginf, isposinf, isfinite
isinf, isneginf, isposinf, isfinite, isnat

Notes
-----
Expand All @@ -1708,6 +1708,41 @@ def add_newdoc(place, name, doc):

""")

add_newdoc('numpy.core.umath', 'isnat',
"""
Test element-wise for NaT (not a time) and return result as a boolean array.

Parameters
----------
x : array_like
Input array with datetime or timedelta data type.

Returns
-------
y : ndarray or bool
For scalar input, the result is a new boolean with value True if
the input is NaT; otherwise the value is False.

For array input, the result is a boolean array of the same
dimensions as the input and the values are True if the
corresponding element of the input is NaT; otherwise the values are
False.

See Also
--------
isnan, isinf, isneginf, isposinf, isfinite

Examples
--------
>>> np.isnat(np.datetime64("NaT"))
True
>>> np.isnat(np.datetime64("2016-01-01"))
False
>>> np.isnat(np.array(["NaT", "2016-01-01"], dtype="datetime64[ns]"))
array([ True, False], dtype=bool)

""")

add_newdoc('numpy.core.umath', 'left_shift',
"""
Shift the bits of an integer to the left.
Expand Down
9 changes: 9 additions & 0 deletions numpy/core/src/umath/loops.c.src
Original file line number Diff line number Diff line change
Expand Up @@ -1227,6 +1227,15 @@ TIMEDELTA_sign(char **args, npy_intp *dimensions, npy_intp *steps, void *NPY_UNU
* #TYPE = DATETIME, TIMEDELTA#
*/

NPY_NO_EXPORT void
@TYPE@_isnat(char **args, npy_intp *dimensions, npy_intp *steps, void *NPY_UNUSED(func))
{
UNARY_LOOP {
const @type@ in1 = *(@type@ *)ip1;
*((npy_bool *)op1) = (in1 == NPY_DATETIME_NAT);
}
}

NPY_NO_EXPORT void
@TYPE@__ones_like(char **args, npy_intp *dimensions, npy_intp *steps, void *NPY_UNUSED(data))
{
Expand Down
3 changes: 3 additions & 0 deletions numpy/core/src/umath/loops.h.src
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,9 @@ TIMEDELTA_sign(char **args, npy_intp *dimensions, npy_intp *steps, void *NPY_UNU
* #TYPE = DATETIME, TIMEDELTA#
*/

NPY_NO_EXPORT void
@TYPE@_isnat(char **args, npy_intp *dimensions, npy_intp *steps, void *NPY_UNUSED(func));

NPY_NO_EXPORT void
@TYPE@__ones_like(char **args, npy_intp *dimensions, npy_intp *steps, void *NPY_UNUSED(data));

Expand Down
26 changes: 26 additions & 0 deletions numpy/core/src/umath/ufunc_type_resolution.c
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,32 @@ PyUFunc_AbsoluteTypeResolver(PyUFuncObject *ufunc,
}
}

/*
* This function applies special type resolution rules for the isnat
* ufunc. This ufunc converts datetime/timedelta -> bool, and is not covered
* by the simple unary type resolution.
*
* Returns 0 on success, -1 on error.
*/
NPY_NO_EXPORT int
PyUFunc_IsNaTTypeResolver(PyUFuncObject *ufunc,
NPY_CASTING casting,
PyArrayObject **operands,
PyObject *type_tup,
PyArray_Descr **out_dtypes)
{
if (!PyTypeNum_ISDATETIME(PyArray_DESCR(operands[0])->type_num)) {
PyErr_SetString(PyExc_ValueError,
"ufunc 'isnat' is only defined for datetime and timedelta.");
return -1;
}

out_dtypes[0] = ensure_dtype_nbo(PyArray_DESCR(operands[0]));
out_dtypes[1] = PyArray_DescrFromType(NPY_BOOL);

return 0;
}

/*
* Creates a new NPY_TIMEDELTA dtype, copying the datetime metadata
* from the given dtype.
Expand Down
7 changes: 7 additions & 0 deletions numpy/core/src/umath/ufunc_type_resolution.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ PyUFunc_AbsoluteTypeResolver(PyUFuncObject *ufunc,
PyArrayObject **operands,
PyObject *type_tup,
PyArray_Descr **out_dtypes);

NPY_NO_EXPORT int
PyUFunc_IsNaTTypeResolver(PyUFuncObject *ufunc,
NPY_CASTING casting,
PyArrayObject **operands,
PyObject *type_tup,
PyArray_Descr **out_dtypes);

NPY_NO_EXPORT int
PyUFunc_AdditionTypeResolver(PyUFuncObject *ufunc,
Expand Down
29 changes: 29 additions & 0 deletions numpy/core/tests/test_datetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -1918,6 +1918,35 @@ def test_datetime_y2038(self):
a = np.datetime64('2038-01-20T13:21:14')
assert_equal(str(a), '2038-01-20T13:21:14')

def test_isnat(self):
assert_(np.isnat(np.datetime64('NaT', 'ms')))
assert_(np.isnat(np.datetime64('NaT', 'ns')))
assert_(not np.isnat(np.datetime64('2038-01-19T03:14:07')))

assert_(np.isnat(np.timedelta64('NaT', "ms")))
assert_(not np.isnat(np.timedelta64(34, "ms")))

res = np.array([False, False, True])
for unit in ['Y', 'M', 'W', 'D',
'h', 'm', 's', 'ms', 'us',
'ns', 'ps', 'fs', 'as']:
arr = np.array([123, -321, "NaT"], dtype='<datetime64[%s]' % unit)
assert_equal(np.isnat(arr), res)
arr = np.array([123, -321, "NaT"], dtype='>datetime64[%s]' % unit)
assert_equal(np.isnat(arr), res)
arr = np.array([123, -321, "NaT"], dtype='<timedelta64[%s]' % unit)
assert_equal(np.isnat(arr), res)
arr = np.array([123, -321, "NaT"], dtype='>timedelta64[%s]' % unit)
assert_equal(np.isnat(arr), res)

def test_isnat_error(self):
# Test that only datetime dtype arrays are accepted
for t in np.typecodes["All"]:
if t in np.typecodes["Datetime"]:
continue
assert_raises(ValueError, np.isnat, np.zeros(10, t))


class TestDateTimeData(TestCase):

def test_basic(self):
Expand Down
4 changes: 2 additions & 2 deletions numpy/ma/tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -1841,11 +1841,11 @@ def test_fillvalue_datetime_timedelta(self):
"h", "D", "W", "M", "Y"):
control = numpy.datetime64("NaT", timecode)
test = default_fill_value(numpy.dtype("<M8[" + timecode + "]"))
assert_equal(test, control)
np.testing.utils.assert_equal(test, control)

control = numpy.timedelta64("NaT", timecode)
test = default_fill_value(numpy.dtype("<m8[" + timecode + "]"))
assert_equal(test, control)
np.testing.utils.assert_equal(test, control)

def test_extremum_fill_value(self):
# Tests extremum fill values for flexible type.
Expand Down
8 changes: 3 additions & 5 deletions numpy/ma/testutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import numpy.core.umath as umath
from numpy.testing import (
TestCase, assert_, assert_allclose, assert_array_almost_equal_nulp,
assert_raises, build_err_msg, run_module_suite, suppress_warnings
assert_raises, build_err_msg, run_module_suite
)
import numpy.testing.utils as utils
from .core import mask_or, getmask, masked_array, nomask, masked, filled
Expand Down Expand Up @@ -126,10 +126,8 @@ def assert_equal(actual, desired, err_msg=''):
return _assert_equal_on_sequences(actual, desired, err_msg='')
if not (isinstance(actual, ndarray) or isinstance(desired, ndarray)):
msg = build_err_msg([actual, desired], err_msg,)
with suppress_warnings() as sup:
sup.filter(FutureWarning, ".*NAT ==")
if not desired == actual:
raise AssertionError(msg)
if not desired == actual:
raise AssertionError(msg)
return
# Case #4. arrays or equivalent
if ((actual is masked) and not (desired is masked)) or \
Expand Down
37 changes: 36 additions & 1 deletion numpy/testing/tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import warnings
import sys
import os
import itertools

import numpy as np
from numpy.testing import (
Expand Down Expand Up @@ -144,7 +145,10 @@ def test_recarrays(self):
c['floupipi'] = a['floupi'].copy()
c['floupa'] = a['floupa'].copy()

self._test_not_equal(c, b)
with suppress_warnings() as sup:
Copy link
Member

Choose a reason for hiding this comment

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

Unrelated fixup?

Copy link
Member Author

Choose a reason for hiding this comment

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

The safe comparison/suppression in the test filtered a bit more then just the NaT stuff (actually may have been there originally not for NaT, but for == operator warnings), so (not actually remembering it) removing it, the tests need to filter these warnings explicitly now. Not sure why this is the only one specifically though.

Copy link
Member

Choose a reason for hiding this comment

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

So this might cause some problems downstream? I'm not terribly concerned, the only way to find out is to try.

Copy link
Member

Choose a reason for hiding this comment

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

Hmm, looks like the FutureWarning will be gone in the 1.14 release.

Copy link
Member Author

Choose a reason for hiding this comment

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

It might cause problems, but maybe its also good, since maybe some project might run into the future warning and did not notice because of this....

l = sup.record(FutureWarning, message="elementwise == ")
self._test_not_equal(c, b)
assert_(len(l) == 1)


class TestBuildErrorMessage(unittest.TestCase):
Expand Down Expand Up @@ -208,6 +212,37 @@ def test_inf_items(self):
self._assert_func([np.inf], [np.inf])
self._test_not_equal(np.inf, [np.inf])

def test_nat_items(self):
# not a datetime
nadt_no_unit = np.datetime64("NaT")
nadt_s = np.datetime64("NaT", "s")
nadt_d = np.datetime64("NaT", "ns")
# not a timedelta
natd_no_unit = np.timedelta64("NaT")
natd_s = np.timedelta64("NaT", "s")
natd_d = np.timedelta64("NaT", "ns")

dts = [nadt_no_unit, nadt_s, nadt_d]
tds = [natd_no_unit, natd_s, natd_d]
for a, b in itertools.product(dts, dts):
self._assert_func(a, b)
self._assert_func([a], [b])
self._test_not_equal([a], b)

for a, b in itertools.product(tds, tds):
self._assert_func(a, b)
self._assert_func([a], [b])
self._test_not_equal([a], b)

for a, b in itertools.product(tds, dts):
self._test_not_equal(a, b)
self._test_not_equal(a, [b])
self._test_not_equal([a], [b])
self._test_not_equal([a], np.datetime64("2017-01-01", "s"))
self._test_not_equal([b], np.datetime64("2017-01-01", "s"))
self._test_not_equal([a], np.timedelta64(123, "s"))
self._test_not_equal([b], np.timedelta64(123, "s"))

def test_non_numeric(self):
self._assert_func('ab', 'ab')
self._test_not_equal('ab', 'abb')
Expand Down
Loading