-
-
Notifications
You must be signed in to change notification settings - Fork 18.4k
Add more overflow tests for timedelta64 operations #46854
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
Conversation
Currently all timedelta64 sums involve int -> float conversion.
Hello @patrickmckenna! Thanks for updating this PR. We checked the lines you've touched for PEP 8 issues, and found: There are currently no PEP 8 issues detected in this Pull Request. Cheers! 🍻 Comment last updated at 2022-05-15 19:13:51 UTC |
1f8ceaa
to
7a35bd2
Compare
7a35bd2
to
2868200
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This PR adds some additional overflow tests for timedeltas. It's goal is to accurately capture the current behavior, not fix existing bugs/inconsistencies—but that's the next step 😄
@jbrockmendel as one who's been working on related issues somewhat recently, I'd appreciate your feedback! (Apologies if there are others I should be pinging, too...) This is my first time poking around pandas/numpy internals, so if there's a better way to write/organize the tests, please LMK.
from pandas.tests.arithmetic.common import ( | ||
assert_invalid_addsub_type, | ||
assert_invalid_comparison, | ||
get_upcast_box, | ||
) | ||
|
||
timedelta_types = (Timedelta, TimedeltaArray, TimedeltaIndex, Series, DataFrame) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Most of the newly-added overflow tests are parameterized over Timedelta
scalar and container types, on the assumption that similar exceptions should be raised for all. (The fact that they're not can be confusing, as mentioned in #43178.) Is that assumption accurate?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
that assumption is accurate
id prefer to keep the Timedelta scalar tests in tests.scalar.timedelta, then use the box_with_array fixture for the non-scalar tests. part of the virtue of this is that when writing the non-scalar tests, we assume that the scalar behavior is correct and tested.
left = wrap_value(Timedelta.max, add_sub_types.left) | ||
right = wrap_value(positive_td, add_sub_types.right) | ||
|
||
if add_sub_types.result is Timedelta: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be raising a OutOfBoundsTimedelta
in all cases? Ref: #34448
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
probably, yes
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks for the tests. these look to be nicely comprehensive and you have a lot of questions about these as we may have tradeoffs in the existing operations. If you can move these to a new file will make it easier to run.
Some td64 overflow tests remain in other modules: - tests/tslibs/test_conversion.py::test_ensure_timedelta64ns_overflows() - tests/tslibs/test_timedeltas.py::test_huge_nanoseconds_overflow() - tests/scalar/timedelta/test_timedelta.py::test_mul_preserves_reso() - tests/scalar/timedelta/test_constructors.py::test_construct_from_td64_with_unit(),test_overflow_on_construction() Still TBD whether these should remain there or also be migrated. See: github.com/pandas-dev/pull/46854#discussion_r858131625
i'll take a look today |
pandas/_testing/__init__.py
Outdated
@@ -279,6 +281,28 @@ def box_expected(expected, box_cls, transpose=True): | |||
return expected | |||
|
|||
|
|||
def wrap_value(value, cls, transpose=False): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The motivation for this was to simplify writing tests that should produce the same output for both scalar and container types, e.g. here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
are there any other thematically similar functions that could be grouped together in a pandas._testing.foo instead of __init__
? we've kind of stagnated, but in principle there's a goal to get stuff out of this file
timestamp = Timestamp("2021-01-01") | ||
result = timestamp + timedelta_range("0s", "1s", periods=31) | ||
expected = DatetimeIndex( | ||
class TestAddSub: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this organization makes sense, I can open a follow up PR to migrate many of the existing tests into this and similar classes (and make them fully parameterized across all container types).
left = wrap_value(Timestamp.min, ts_add_sub_types[1]) | ||
|
||
ex = (OutOfBoundsDatetime, OverflowError) | ||
msg = "|".join(["Out of bounds nanosecond timestamp", "Overflow in int64 addition"]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed. Imagine this would be part of the same future PR mentioned in #46854 (comment)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jbrockmendel @jreback per #46854 (comment), I've migrated the new tests back to existing modules. I've also removed the hypothesis usage, and rewritten the tests to follow the pattern of accepting box types as fixtures. Please LMK if you see anything else that needs changing.
Once this ships, I can open up a follow-on PR to make the exception raising more consistent.
) | ||
tm.assert_index_equal(result, expected) | ||
def test_sub_raises_if_result_would_overflow( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FWIW, I tried rewriting this to keep the test function body constant, and mapping each input value to an appropriate xfail
marker:
xfail_does_not_raise = pytest.mark.xfail(reason="doesn't raise", strict=True)
xfail_raises_overflow_error = pytest.mark.xfail(
reason="raises wrong error",
raises=OverflowError,
strict=True,
)
# ...
@pytest.mark.parametrize(
"rval",
[
pytest.param(Timedelta(1), marks=xfail_does_not_raise),
pytest.param(Timedelta(2), marks=xfail_raises_overflow_error),
pytest.param(Timedelta.max, marks=xfail_raises_overflow_error),
],
)
def test_sub_raises_td64_specific_error_if_result_would_overflow(
self,
max_td64: TD64_BOX_TYPE,
rval: Timedelta,
td64_type: Type[TD64_TYPE],
):
rvalue = tm.wrap_value(rval, td64_type)
min_td64 = -1 * max_td64
with pytest.raises(OutOfBoundsTimedelta, match="too small"):
min_td64 - rvalue
Unfortunately that slowed execution by a factor of ~40 (locally). Decorating the test with a single, catch-all xfail
marker led to the same results. Passing run=False
did help, but it was still ~5x slower.
will take another look tomorrow |
from functools import partial | ||
from typing import ( | ||
Type, | ||
Union, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can't use type
and |
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wish, that's a 3.10
feature.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
even if we do from __future__ import annotations
? im sure we use pipes elsewhere
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Huh, I'd tried that and switching to pipes, and it was failing for type aliases like TD64_BOX_TYPE = TimedeltaArray | TimedeltaIndex | Series | DataFrame
. But TIL: pipes will work for function annotations (with that import
).
type
does work. I'll switch to that, and replace the aliases with pipes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Huh, I'd tried that and switching to pipes, and it was failing for type aliases like TD64_BOX_TYPE = TimedeltaArray | TimedeltaIndex | Series | DataFrame
. But TIL: pipes will work for function annotations (with that import
).
type
does work. I'll switch to that, and replace the aliases with pipes.
""" | ||
dt64 = tm.wrap_value(Timestamp.now(), dt64_type) | ||
td64 = tm.wrap_value(Timedelta(0), td64_type) | ||
return type(dt64 + td64) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
so this is going to map Timedelta->Timestamp and TimedeltaArray->DatetimeArray but be an identity mapping for everything else?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I might be misunderstanding the question, but think the gist of what you're suggesting is right (assuming Array stands for every box type). So:
Timedelta + Timestamp -> Timestamp
Timedelta + DatetimeArray -> DatetimeArray
Timedelta + DatetimeIndex -> DatetimeIndex
Timedelta + Series -> Series
Timedelta + DataFrame -> DataFrame
TimedeltaArray + Timestamp -> DatetimeArray
TimedeltaArray + DatetimeArray -> DatetimeArray
...
TimedeltaIndex + Timestamp -> DatetimeIndex
TimedeltaIndex + DatetimeArray -> DatetimeIndex
...
Happy to update the docstring, or use a different pattern, if that's unclear.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My preference (mentioned #46854 (comment)) is to handle the Timedelta/Timestamp cases in the existing tests/scalar/... files, and the non-scalar tests here. Then this wrapping becomes unnecessary and you can just use box_with_array
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about scalar/box tests, e.g. Timedelta + DatetimeIndex
, where should those live?
@jbrockmendel happy for you to merge this when you are ready |
@jreback appreciate that! Alas, no green button for me—I haven't got write access 🙃 |
@patrickmckenna that was to @jbrockmendel :-> |
Hah, whoops! 🤦♂️😅 |
Please LMK if there's more that needs doing here. Unsure what to do about the few red x's, which appear to be (transient?) failures during CI setup. |
This pull request is stale because it has been open for thirty days with no activity. Please update and respond to this comment if you're still interested in working on this. |
Thanks for the pull request, but it appears to have gone stale. If interested in continuing, please merge in the main branch, address any review comments and/or failing tests, and we can reopen. |
doc/source/whatsnew/vX.X.X.rst
file if fixing a bug or adding a new feature.This PR adds more specific tests, to verify the current, sometimes inconsistent behavior. Much of it has been reported/discussed in previous threads, and some appears to reflect intentional tradeoffs, e.g. allowing rounding error to avoid overflows. Other behavior seems likely unintentional:
ValueError
andOverflowError
OverflowError
s for 1-elementSeries
w/ valid valuesSince this is my first time poking around pandas/numpy internals, it seemed best to make sure the right tests are in the right places before attempting any fixes.
Open Questions
Series.sum()
would yield an invalidTimedelta
:ValueError
,OutOfBoundsTimedelta
, something else? (I can rewrite the tests to mark those cases that currently raise the "wrong" exception asxfail
for now.)pandas.core.nanops
, particularly special-casing oftimedelta64
innansum()
:pandas/pandas/core/nanops.py
Lines 618 to 621 in 4cf8d55
Prior Art