Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ but cannot always guarantee backwards compatibility. Changes that may **break co

**Improved**

- Added `TimeSeries.idxmin()` and `TimeSeries.idxmax()`, returning a `pandas.Series` (indexed by component) of the time index value at which each component attains its minimum / maximum. Also clarified the docstrings of `min()` / `max()` to point users to these new helpers when they want the actual argmin/argmax timestamp. Closes [#2696](https://github.com/unit8co/darts/issues/2696).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
- Added `TimeSeries.idxmin()` and `TimeSeries.idxmax()`, returning a `pandas.Series` (indexed by component) of the time index value at which each component attains its minimum / maximum. Also clarified the docstrings of `min()` / `max()` to point users to these new helpers when they want the actual argmin/argmax timestamp. Closes [#2696](https://github.com/unit8co/darts/issues/2696).
- Added `TimeSeries.idxmin()` and `TimeSeries.idxmax()`, returning a `pandas.Series` (indexed by component) of the time index value at which each component attains its minimum / maximum. Closes [#3115](https://github.com/unit8co/darts/pull/3115).


**Fixed**

**Dependencies**
Expand Down
59 changes: 59 additions & 0 deletions darts/tests/test_timeseries.py
Original file line number Diff line number Diff line change
Expand Up @@ -3297,6 +3297,65 @@ def test_max(self):
new_ts._values, self.values.max(axis=axis, keepdims=True)
).all()

def test_idxmin_idxmax_univariate_datetime(self):
# univariate, deterministic, datetime index — covers issue #2696
# where TimeSeries.min(axis=0) returns the first timestamp instead of
# the timestamp of the actual minimum.
idx = pd.date_range("2020-01-01", periods=5, freq="D")
values = np.array([3.0, 1.0, 4.0, 0.0, 2.0])
ts = TimeSeries(times=idx, values=values, components=["a"])

# idxmin / idxmax return a pd.Series indexed by component name.
idxmin = ts.idxmin()
idxmax = ts.idxmax()
assert list(idxmin.index) == ["a"]
assert list(idxmax.index) == ["a"]
# The actual minimum is at position 3 (2020-01-04) and the maximum at
# position 2 (2020-01-03). This is the load-bearing assertion: it
# fails on master where users had to fall back to pd_dataframe()
# because TimeSeries provided no idx{min,max}.
assert idxmin["a"] == pd.Timestamp("2020-01-04")
assert idxmax["a"] == pd.Timestamp("2020-01-03")

def test_idxmin_idxmax_multivariate(self):
# different argmin/argmax per component
idx = pd.date_range("2020-01-01", periods=3, freq="D")
values = np.array([[1.0, 0.0], [0.0, 0.0], [0.0, 1.0]])
ts = TimeSeries(times=idx, values=values, components=["a", "b"])

idxmin = ts.idxmin()
idxmax = ts.idxmax()
# argmin returns first occurrence of the minimum, mirroring numpy.
assert idxmin["a"] == pd.Timestamp("2020-01-02")
assert idxmin["b"] == pd.Timestamp("2020-01-01")
assert idxmax["a"] == pd.Timestamp("2020-01-01")
assert idxmax["b"] == pd.Timestamp("2020-01-03")

def test_idxmin_idxmax_range_index(self):
# RangeIndex-based series should return integer indices.
values = np.array([5.0, 3.0, 8.0, 1.0])
ts = TimeSeries(
times=pd.RangeIndex(start=10, stop=14, step=1),
values=values,
components=["x"],
)
assert ts.idxmin()["x"] == 13
assert ts.idxmax()["x"] == 12

def test_idxmin_idxmax_stochastic(self):
# For a stochastic series we reduce samples with the median first so
# the answer does not depend on how many samples were drawn.
rng = np.random.default_rng(0)
idx = pd.date_range("2020-01-01", periods=4, freq="D")
# Component "a" has its median minimum at t=2.
median_target = np.array([5.0, 3.0, 1.0, 2.0])
values = np.stack(
[median_target + rng.normal(0, 0.01, size=4) for _ in range(50)],
axis=-1,
)[:, None, :]
ts = TimeSeries(times=idx, values=values, components=["a"])
assert ts.idxmin()["a"] == pd.Timestamp("2020-01-03")

Comment thread
dennisbader marked this conversation as resolved.
def test_sum(self):
for axis in range(3):
new_ts = self.ts.sum(axis=axis)
Expand Down
92 changes: 92 additions & 0 deletions darts/timeseries.py
Original file line number Diff line number Diff line change
Expand Up @@ -4764,6 +4764,12 @@ def min(self, axis: int = 2) -> Self:

If ``axis=1``, the static covariates and the hierarchy are discarded from the series.

.. note::
With ``axis=0`` the returned timestamp is the first entry of the
original ``time_index`` and **does not** correspond to the
timestamp of the actual minimum value. Use :func:`idxmin` to get
the timestamp at which each component attains its minimum.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

i'd say this is redudnant with the rest of the docstring as the behavior is already mentioned above (`If we reduce over time (axis=0), the series will have length one and will use the first entry of the
original ``time_index```)


Parameters
----------
axis
Expand Down Expand Up @@ -4793,6 +4799,12 @@ def max(self, axis: int = 2) -> Self:

If ``axis=1``, the static covariates and the hierarchy are discarded from the series.

.. note::
With ``axis=0`` the returned timestamp is the first entry of the
original ``time_index`` and **does not** correspond to the
timestamp of the actual maximum value. Use :func:`idxmax` to get
the timestamp at which each component attains its maximum.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

same case as above for min

Parameters
----------
axis
Expand All @@ -4812,6 +4824,86 @@ def max(self, axis: int = 2) -> Self:
**(self._attrs if axis != 1 else dict()),
)

def idxmin(self) -> pd.Series:
"""Return the time index value of the minimum of each component.

For a stochastic series the median over samples is taken before
finding the minimum, so the returned index is well-defined regardless
of ``n_samples``.

Useful as a companion to :func:`min` because ``min(axis=0)`` returns a
single-row series whose timestamp is the *first* time index entry of
the original series, not the entry of the actual minimum (see
`issue #2696 <https://github.com/unit8co/darts/issues/2696>`_).

Comment on lines +4823 to +4824

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

i'd say this is unnecesssary

Returns
-------
pandas.Series
A series indexed by component name. Each value is the timestamp
(or integer index, if the series uses an ``RangeIndex``) at which
that component attains its minimum.

Examples
--------
>>> import pandas as pd
>>> from darts import TimeSeries
>>> df = pd.DataFrame({"a": [1, 0, 0], "b": [0, 0, 1]})
>>> series = TimeSeries.from_dataframe(df)
>>> series.idxmin()
a 1
b 0
dtype: int64
"""
# Reduce samples first so the result is independent of stochasticity;
# using the median (rather than mean) keeps the returned index value
# an actual observed value when n_samples == 1.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

unnecessary, already mentioned in the docstring

deterministic = (
self._values
if self.is_deterministic
else np.median(self._values, axis=2, keepdims=True)
)
# argmin along time axis → shape (n_components,)
idxs = deterministic[:, :, 0].argmin(axis=0)
return pd.Series(self._time_index[idxs], index=self.components)

def idxmax(self) -> pd.Series:
"""Return the time index value of the maximum of each component.

For a stochastic series the median over samples is taken before
finding the maximum, so the returned index is well-defined regardless
of ``n_samples``.

Useful as a companion to :func:`max` because ``max(axis=0)`` returns a
single-row series whose timestamp is the *first* time index entry of
the original series, not the entry of the actual maximum (see
`issue #2696 <https://github.com/unit8co/darts/issues/2696>`_).

Comment on lines +4858 to +4859

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

same as above

Returns
-------
pandas.Series
A series indexed by component name. Each value is the timestamp
(or integer index, if the series uses an ``RangeIndex``) at which
that component attains its maximum.

Examples
--------
>>> import pandas as pd
>>> from darts import TimeSeries
>>> df = pd.DataFrame({"a": [1, 0, 0], "b": [0, 0, 1]})
>>> series = TimeSeries.from_dataframe(df)
>>> series.idxmax()
a 0
b 2
dtype: int64
"""
deterministic = (
self._values
if self.is_deterministic
else np.median(self._values, axis=2, keepdims=True)
)
idxs = deterministic[:, :, 0].argmax(axis=0)
return pd.Series(self._time_index[idxs], index=self.components)

def quantile(self, q: float | Sequence[float] = 0.5, **kwargs) -> Self:
"""Return a deterministic series with the desired quantile(s) `q` of each component computed over the samples
of the stochastic series.
Expand Down