Skip to content

ENH: mul(Tick, float); simplify to_offset #34486

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 5 commits into from
Jun 1, 2020
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
1 change: 1 addition & 0 deletions doc/source/whatsnew/v1.1.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -994,6 +994,7 @@ Other
- Bug in :meth:`DataFrame.plot.scatter` caused an error when plotting variable marker sizes (:issue:`32904`)
- :class:`IntegerArray` now implements the ``sum`` operation (:issue:`33172`)
- Bug in :class:`Tick` comparisons raising ``TypeError`` when comparing against timedelta-like objects (:issue:`34088`)
- Bug in :class:`Tick` multiplication raising ``TypeError`` when multiplying by a float (:issue:`34486`)

.. ---------------------------------------------------------------------------

Expand Down
71 changes: 60 additions & 11 deletions pandas/_libs/tslibs/offsets.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@ cnp.import_array()
from pandas._libs.properties import cache_readonly

from pandas._libs.tslibs cimport util
from pandas._libs.tslibs.util cimport is_integer_object, is_datetime64_object
from pandas._libs.tslibs.util cimport (
is_integer_object,
is_datetime64_object,
is_float_object,
)

from pandas._libs.tslibs.base cimport ABCTimestamp

Expand Down Expand Up @@ -743,6 +747,25 @@ cdef class Tick(SingleConstructorOffset):
"Tick offset with `normalize=True` are not allowed."
)

# FIXME: Without making this cpdef, we get AttributeError when calling
# from __mul__
cpdef Tick _next_higher_resolution(Tick self):
Copy link
Contributor

Choose a reason for hiding this comment

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

wouldn't this be better as a module level routine?

Copy link
Member Author

Choose a reason for hiding this comment

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

i dont think so. it uses self.n, so really makes sense as a method

if type(self) is Day:
return Hour(self.n * 24)
if type(self) is Hour:
return Minute(self.n * 60)
if type(self) is Minute:
return Second(self.n * 60)
if type(self) is Second:
return Milli(self.n * 1000)
if type(self) is Milli:
return Micro(self.n * 1000)
if type(self) is Micro:
return Nano(self.n * 1000)
raise NotImplementedError(type(self))

# --------------------------------------------------------------------

def _repr_attrs(self) -> str:
# Since cdef classes have no __dict__, we need to override
return ""
Expand Down Expand Up @@ -791,6 +814,21 @@ cdef class Tick(SingleConstructorOffset):
def __gt__(self, other):
return self.delta.__gt__(other)

def __mul__(self, other):
if not isinstance(self, Tick):
# cython semantics, this is __rmul__
return other.__mul__(self)
if is_float_object(other):
n = other * self.n
# If the new `n` is an integer, we can represent it using the
# same Tick subclass as self, otherwise we need to move up
# to a higher-resolution subclass
if np.isclose(n % 1, 0):
return type(self)(int(n))
new_self = self._next_higher_resolution()
return new_self * other
return BaseOffset.__mul__(self, other)

def __truediv__(self, other):
if not isinstance(self, Tick):
# cython semantics mean the args are sometimes swapped
Expand Down Expand Up @@ -3563,6 +3601,9 @@ cpdef to_offset(freq):
>>> to_offset(Hour())
<Hour>
"""
# TODO: avoid runtime imports
Copy link
Contributor

Choose a reason for hiding this comment

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

really really prefer absolute imports

Copy link
Member Author

Choose a reason for hiding this comment

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

sure. im more concerned about getting rid of the runtime imports

from pandas._libs.tslibs.timedeltas import Timedelta

if freq is None:
return None

Expand All @@ -3589,7 +3630,9 @@ cpdef to_offset(freq):
if split[-1] != "" and not split[-1].isspace():
# the last element must be blank
raise ValueError("last element must be blank")
for sep, stride, name in zip(split[0::4], split[1::4], split[2::4]):

tups = zip(split[0::4], split[1::4], split[2::4])
for n, (sep, stride, name) in enumerate(tups):
if sep != "" and not sep.isspace():
raise ValueError("separator must be spaces")
prefix = _lite_rule_alias.get(name) or name
Expand All @@ -3598,16 +3641,22 @@ cpdef to_offset(freq):
if not stride:
stride = 1

Copy link
Contributor

Choose a reason for hiding this comment

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

can you give some comments on what is being done here

# TODO: avoid runtime import
from .resolution import Resolution, reso_str_bump_map
if prefix in {"D", "H", "T", "S", "L", "U", "N"}:
# For these prefixes, we have something like "3H" or
# "2.5T", so we can construct a Timedelta with the
# matching unit and get our offset from delta_to_tick
td = Timedelta(1, unit=prefix)
off = delta_to_tick(td)
offset = off * float(stride)
if n != 0:
# If n==0, then stride_sign is already incorporated
# into the offset
offset *= stride_sign
else:
stride = int(stride)
offset = _get_offset(name)
offset = offset * int(np.fabs(stride) * stride_sign)

if prefix in reso_str_bump_map:
stride, name = Resolution.get_stride_from_decimal(
float(stride), prefix
)
stride = int(stride)
offset = _get_offset(name)
offset = offset * int(np.fabs(stride) * stride_sign)
if delta is None:
delta = offset
else:
Expand Down
16 changes: 16 additions & 0 deletions pandas/tests/tseries/offsets/test_ticks.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,22 @@ def test_tick_division(cls):
assert result.delta == off.delta / 0.001


def test_tick_mul_float():
off = Micro(2)

# Case where we retain type
result = off * 1.5
expected = Micro(3)
assert result == expected
assert isinstance(result, Micro)

# Case where we bump up to the next type
result = off * 1.25
Copy link
Contributor

Choose a reason for hiding this comment

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

was this buggy before? need a whatsnew note if so

Copy link
Member Author

Choose a reason for hiding this comment

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

ATM it raises TypeError for any float, will add whatsnew

expected = Nano(2500)
assert result == expected
assert isinstance(result, Nano)


@pytest.mark.parametrize("cls", tick_classes)
def test_tick_rdiv(cls):
off = cls(10)
Expand Down