Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pandas/core/resample.py: 25%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1from __future__ import annotations
3import copy
4from typing import (
5 TYPE_CHECKING,
6 Concatenate,
7 Literal,
8 Self,
9 cast,
10 final,
11 no_type_check,
12 overload,
13)
14import warnings
16import numpy as np
18from pandas._libs import lib
19from pandas._libs.tslibs import (
20 BaseOffset,
21 IncompatibleFrequency,
22 NaT,
23 Period,
24 Timedelta,
25 Timestamp,
26 to_offset,
27)
28from pandas.errors import (
29 AbstractMethodError,
30 Pandas4Warning,
31)
32from pandas.util._decorators import set_module
33from pandas.util._exceptions import find_stack_level
35from pandas.core.dtypes.dtypes import (
36 ArrowDtype,
37 PeriodDtype,
38)
39from pandas.core.dtypes.generic import (
40 ABCDataFrame,
41 ABCSeries,
42)
44import pandas.core.algorithms as algos
45from pandas.core.apply import ResamplerWindowApply
46from pandas.core.base import (
47 PandasObject,
48 SelectionMixin,
49)
50from pandas.core.generic import (
51 NDFrame,
52)
53from pandas.core.groupby.groupby import (
54 BaseGroupBy,
55 GroupBy,
56 get_groupby,
57)
58from pandas.core.groupby.grouper import Grouper
59from pandas.core.groupby.ops import BinGrouper
60from pandas.core.indexes.api import MultiIndex
61from pandas.core.indexes.base import Index
62from pandas.core.indexes.datetimes import (
63 DatetimeIndex,
64 date_range,
65)
66from pandas.core.indexes.period import (
67 PeriodIndex,
68 period_range,
69)
70from pandas.core.indexes.timedeltas import (
71 TimedeltaIndex,
72 timedelta_range,
73)
74from pandas.core.reshape.concat import concat
76from pandas.tseries.frequencies import (
77 is_subperiod,
78 is_superperiod,
79)
80from pandas.tseries.offsets import (
81 Day,
82 Hour,
83 Tick,
84)
86if TYPE_CHECKING:
87 from collections.abc import (
88 Callable,
89 Hashable,
90 )
92 from pandas._typing import (
93 Any,
94 AnyArrayLike,
95 Axis,
96 FreqIndexT,
97 Frequency,
98 IndexLabel,
99 InterpolateOptions,
100 NDFrameT,
101 P,
102 T,
103 TimedeltaConvertibleTypes,
104 TimeGrouperOrigin,
105 TimestampConvertibleTypes,
106 TimeUnit,
107 npt,
108 )
110 from pandas import (
111 DataFrame,
112 Series,
113 )
114 from pandas.core.arrays import ArrowExtensionArray
115 from pandas.core.generic import NDFrame
118@set_module("pandas.api.typing")
119class Resampler(BaseGroupBy, PandasObject):
120 """
121 Class for resampling datetimelike data, a groupby-like operation.
122 See aggregate, transform, and apply functions on this object.
124 It's easiest to use obj.resample(...) to use Resampler.
126 Parameters
127 ----------
128 obj : Series or DataFrame
129 groupby : TimeGrouper
131 Returns
132 -------
133 a Resampler of the appropriate type
135 Notes
136 -----
137 After resampling, see aggregate, apply, and transform functions.
138 """
140 _grouper: BinGrouper
141 _timegrouper: TimeGrouper
142 binner: DatetimeIndex | TimedeltaIndex | PeriodIndex # depends on subclass
143 exclusions: frozenset[Hashable] = frozenset() # for SelectionMixin compat
144 _internal_names_set = set({"obj", "ax", "_indexer"})
146 # to the groupby descriptor
147 _attributes = [
148 "freq",
149 "closed",
150 "label",
151 "convention",
152 "origin",
153 "offset",
154 ]
156 def __init__(
157 self,
158 obj: NDFrame,
159 timegrouper: TimeGrouper,
160 *,
161 gpr_index: Index,
162 group_keys: bool = False,
163 selection=None,
164 include_groups: bool = False,
165 ) -> None:
166 if include_groups:
167 raise ValueError("include_groups=True is no longer allowed.")
168 self._timegrouper = timegrouper
169 self.keys = None
170 self.sort = True
171 self.group_keys = group_keys
172 self.as_index = True
174 self.obj, self.ax, self._indexer = self._timegrouper._set_grouper(
175 self._convert_obj(obj), sort=True, gpr_index=gpr_index
176 )
177 self.binner, self._grouper = self._get_binner()
178 self._selection = selection
179 if self._timegrouper.key is not None:
180 self.exclusions = frozenset([self._timegrouper.key])
181 else:
182 self.exclusions = frozenset()
184 @final
185 def __str__(self) -> str:
186 """
187 Provide a nice str repr of our rolling object.
188 """
189 attrs = (
190 f"{k}={getattr(self._timegrouper, k)}"
191 for k in self._attributes
192 if getattr(self._timegrouper, k, None) is not None
193 )
194 return f"{type(self).__name__} [{', '.join(attrs)}]"
196 @final
197 def __getattr__(self, attr: str):
198 if attr in self._internal_names_set:
199 return object.__getattribute__(self, attr)
200 if attr in self._attributes:
201 return getattr(self._timegrouper, attr)
202 if attr in self.obj:
203 return self[attr]
205 return object.__getattribute__(self, attr)
207 @final
208 @property
209 def _from_selection(self) -> bool:
210 """
211 Is the resampling from a DataFrame column or MultiIndex level.
212 """
213 # upsampling and PeriodIndex resampling do not work
214 # with selection, this state used to catch and raise an error
215 return self._timegrouper is not None and (
216 self._timegrouper.key is not None or self._timegrouper.level is not None
217 )
219 def _convert_obj(self, obj: NDFrameT) -> NDFrameT:
220 """
221 Provide any conversions for the object in order to correctly handle.
223 Parameters
224 ----------
225 obj : Series or DataFrame
227 Returns
228 -------
229 Series or DataFrame
230 """
231 return obj._consolidate()
233 def _get_binner_for_time(self):
234 raise AbstractMethodError(self)
236 @final
237 def _get_binner(self):
238 """
239 Create the BinGrouper, assume that self.set_grouper(obj)
240 has already been called.
241 """
242 binner, bins, binlabels = self._get_binner_for_time()
243 assert len(bins) == len(binlabels)
244 if self._timegrouper._arrow_dtype is not None:
245 binlabels = binlabels.astype(self._timegrouper._arrow_dtype)
246 bin_grouper = BinGrouper(bins, binlabels, indexer=self._indexer)
247 bin_grouper._is_resample = True
248 return binner, bin_grouper
250 @overload
251 def pipe(
252 self,
253 func: Callable[Concatenate[Self, P], T],
254 *args: P.args,
255 **kwargs: P.kwargs,
256 ) -> T: ...
258 @overload
259 def pipe(
260 self,
261 func: tuple[Callable[..., T], str],
262 *args: Any,
263 **kwargs: Any,
264 ) -> T: ...
266 @final
267 def pipe(
268 self,
269 func: Callable[Concatenate[Self, P], T] | tuple[Callable[..., T], str],
270 *args: Any,
271 **kwargs: Any,
272 ) -> T:
273 """
274 Apply a ``func`` with arguments to this Resampler object and return its result.
276 Use `.pipe` when you want to improve readability by chaining together
277 functions that expect Series, DataFrames, GroupBy or Resampler objects.
278 Instead of writing
280 >>> h = lambda x, arg2, arg3: x + 1 - arg2 * arg3
281 >>> g = lambda x, arg1: x * 5 / arg1
282 >>> f = lambda x: x**4
283 >>> df = pd.DataFrame([["a", 4], ["b", 5]], columns=["group", "value"])
284 >>> h(g(f(df.groupby("group")), arg1=1), arg2=2, arg3=3) # doctest: +SKIP
286 You can write
288 >>> (
289 ... df.groupby("group").pipe(f).pipe(g, arg1=1).pipe(h, arg2=2, arg3=3)
290 ... ) # doctest: +SKIP
292 which is much more readable.
294 Parameters
295 ----------
296 func : callable or tuple of (callable, str)
297 Function to apply to this Resampler object or, alternatively,
298 a `(callable, data_keyword)` tuple where `data_keyword` is a
299 string indicating the keyword of `callable` that expects the
300 Resampler object.
301 *args : iterable, optional
302 Positional arguments passed into `func`.
303 **kwargs : dict, optional
304 A dictionary of keyword arguments passed into `func`.
306 Returns
307 -------
308 any
309 The result of applying ``func`` to the Resampler object.
311 See Also
312 --------
313 Series.pipe : Apply a function with arguments to a series.
314 DataFrame.pipe: Apply a function with arguments to a dataframe.
315 apply : Apply function to each group instead of to the
316 full Resampler object.
318 Notes
319 -----
320 See more `here
321 <https://pandas.pydata.org/pandas-docs/stable/user_guide/groupby.html#piping-function-calls>`_
323 Examples
324 --------
325 >>> df = pd.DataFrame(
326 ... {"A": [1, 2, 3, 4]}, index=pd.date_range("2012-08-02", periods=4)
327 ... )
328 >>> df
329 A
330 2012-08-02 1
331 2012-08-03 2
332 2012-08-04 3
333 2012-08-05 4
335 To get the difference between each 2-day period's maximum and minimum
336 value in one pass, you can do
338 >>> df.resample("2D").pipe(lambda x: x.max() - x.min())
339 A
340 2012-08-02 1
341 2012-08-04 1
342 """
343 return super().pipe(func, *args, **kwargs)
345 @final
346 def aggregate(self, func=None, *args, **kwargs):
347 """
348 Aggregate using one or more operations over the specified axis.
350 This method applies aggregation functions to resampled groups, enabling
351 summary statistics to be computed for each time period.
353 Parameters
354 ----------
355 func : function, str, list or dict
356 Function to use for aggregating the data. If a function, must either
357 work when passed a DataFrame or when passed to DataFrame.apply.
359 Accepted combinations are:
361 - function
362 - string function name
363 - list of functions and/or function names, e.g. ``[np.sum, 'mean']``
364 - dict of axis labels -> functions, function names or list of such.
365 *args
366 Positional arguments to pass to `func`.
367 **kwargs
368 Keyword arguments to pass to `func`.
370 Returns
371 -------
372 scalar, Series or DataFrame
374 The return can be:
376 * scalar : when Series.agg is called with single function
377 * Series : when DataFrame.agg is called with a single function
378 * DataFrame : when DataFrame.agg is called with several functions
380 See Also
381 --------
382 DataFrame.groupby.aggregate : Aggregate using callable, string, dict,
383 or list of string/callables.
384 DataFrame.resample.transform : Transforms the Series on each group
385 based on the given function.
386 DataFrame.aggregate: Aggregate using one or more
387 operations over the specified axis.
389 Notes
390 -----
391 The aggregation operations are always performed over an axis, either the
392 index (default) or the column axis. This behavior is different from
393 `numpy` aggregation functions (`mean`, `median`, `prod`, `sum`, `std`,
394 `var`), where the default is to compute the aggregation of the flattened
395 array, e.g., ``numpy.mean(arr_2d)`` as opposed to
396 ``numpy.mean(arr_2d, axis=0)``.
398 `agg` is an alias for `aggregate`. Use the alias.
400 Functions that mutate the passed object can produce unexpected
401 behavior or errors and are not supported. See :ref:`gotchas.udf-mutation`
402 for more details.
404 A passed user-defined-function will be passed a Series for evaluation.
406 If ``func`` defines an index relabeling, ``axis`` must be ``0`` or ``index``.
408 Examples
409 --------
410 >>> s = pd.Series(
411 ... [1, 2, 3, 4, 5], index=pd.date_range("20130101", periods=5, freq="s")
412 ... )
413 >>> s
414 2013-01-01 00:00:00 1
415 2013-01-01 00:00:01 2
416 2013-01-01 00:00:02 3
417 2013-01-01 00:00:03 4
418 2013-01-01 00:00:04 5
419 Freq: s, dtype: int64
421 >>> r = s.resample("2s")
423 >>> r.agg("sum")
424 2013-01-01 00:00:00 3
425 2013-01-01 00:00:02 7
426 2013-01-01 00:00:04 5
427 Freq: 2s, dtype: int64
429 >>> r.agg(["sum", "mean", "max"])
430 sum mean max
431 2013-01-01 00:00:00 3 1.5 2
432 2013-01-01 00:00:02 7 3.5 4
433 2013-01-01 00:00:04 5 5.0 5
435 >>> r.agg({"result": lambda x: x.mean() / x.std(), "total": "sum"})
436 result total
437 2013-01-01 00:00:00 2.121320 3
438 2013-01-01 00:00:02 4.949747 7
439 2013-01-01 00:00:04 NaN 5
441 >>> r.agg(average="mean", total="sum")
442 average total
443 2013-01-01 00:00:00 1.5 3
444 2013-01-01 00:00:02 3.5 7
445 2013-01-01 00:00:04 5.0 5
446 """
447 result = ResamplerWindowApply(self, func, args=args, kwargs=kwargs).agg()
448 if result is None:
449 how = func
450 result = self._groupby_and_aggregate(how, *args, **kwargs)
452 return result
454 agg = aggregate
455 apply = aggregate
457 @final
458 def transform(self, arg, *args, **kwargs):
459 """
460 Call function producing a like-indexed Series on each group.
462 Return a Series with the transformed values.
464 Parameters
465 ----------
466 arg : function
467 To apply to each group. Should return a Series with the same index.
468 *args, **kwargs
469 Additional arguments and keywords.
471 Returns
472 -------
473 Series
474 A Series with the transformed values, maintaining the same index as
475 the original object.
477 See Also
478 --------
479 core.resample.Resampler.apply : Apply a function along each group.
480 core.resample.Resampler.aggregate : Aggregate using one or more operations
481 over the specified axis.
483 Examples
484 --------
485 >>> s = pd.Series([1, 2], index=pd.date_range("20180101", periods=2, freq="1h"))
486 >>> s
487 2018-01-01 00:00:00 1
488 2018-01-01 01:00:00 2
489 Freq: h, dtype: int64
491 >>> resampled = s.resample("15min")
492 >>> resampled.transform(lambda x: (x - x.mean()) / x.std())
493 2018-01-01 00:00:00 NaN
494 2018-01-01 01:00:00 NaN
495 Freq: h, dtype: float64
496 """
497 return self._selected_obj.groupby(self._timegrouper).transform(
498 arg, *args, **kwargs
499 )
501 def _downsample(self, how, **kwargs):
502 raise AbstractMethodError(self)
504 def _upsample(self, f, limit: int | None = None, fill_value=None):
505 raise AbstractMethodError(self)
507 def _gotitem(self, key, ndim: int, subset=None):
508 """
509 Sub-classes to define. Return a sliced object.
511 Parameters
512 ----------
513 key : string / list of selections
514 ndim : {1, 2}
515 requested ndim of result
516 subset : object, default None
517 subset to act on
518 """
519 grouper = self._grouper
520 if subset is None:
521 subset = self.obj
522 if key is not None:
523 subset = subset[key]
524 else:
525 # reached via Apply.agg_dict_like with selection=None and ndim=1
526 assert subset.ndim == 1
527 if ndim == 1:
528 assert subset.ndim == 1
530 grouped = get_groupby(
531 subset, by=None, grouper=grouper, group_keys=self.group_keys
532 )
533 return grouped
535 def _groupby_and_aggregate(self, how, *args, **kwargs):
536 """
537 Re-evaluate the obj with a groupby aggregation.
538 """
539 grouper = self._grouper
541 # Excludes `on` column when provided
542 obj = self._obj_with_exclusions
544 grouped = get_groupby(obj, by=None, grouper=grouper, group_keys=self.group_keys)
546 try:
547 if callable(how):
548 # TODO: test_resample_apply_with_additional_args fails if we go
549 # through the non-lambda path, not clear that it should.
550 func = lambda x: how(x, *args, **kwargs)
551 result = grouped.aggregate(func)
552 else:
553 result = grouped.aggregate(how, *args, **kwargs)
554 except (AttributeError, KeyError):
555 # we have a non-reducing function; try to evaluate
556 # alternatively we want to evaluate only a column of the input
558 # test_apply_to_one_column_of_df the function being applied references
559 # a DataFrame column, but aggregate_item_by_item operates column-wise
560 # on Series, raising AttributeError or KeyError
561 # (depending on whether the column lookup uses getattr/__getitem__)
562 result = grouped.apply(how, *args, **kwargs)
564 except ValueError as err:
565 if "Must produce aggregated value" in str(err):
566 # raised in _aggregate_named
567 # see test_apply_without_aggregation, test_apply_with_mutated_index
568 pass
569 else:
570 raise
572 # we have a non-reducing function
573 # try to evaluate
574 result = grouped.apply(how, *args, **kwargs)
576 return self._wrap_result(result)
578 @final
579 def _get_resampler_for_grouping(
580 self,
581 groupby: GroupBy,
582 key,
583 ):
584 """
585 Return the correct class for resampling with groupby.
586 """
587 return self._resampler_for_grouping(
588 groupby=groupby,
589 key=key,
590 parent=self,
591 )
593 def _wrap_result(self, result):
594 """
595 Potentially wrap any results.
596 """
597 if isinstance(result, ABCSeries) and self._selection is not None:
598 result.name = self._selection
600 if isinstance(result, ABCSeries) and result.empty:
601 # When index is all NaT, result is empty but index is not
602 obj = self.obj
603 result.index = _asfreq_compat(obj.index[:0], freq=self.freq)
604 result.name = getattr(obj, "name", None)
606 if self._timegrouper._arrow_dtype is not None:
607 result.index = result.index.astype(self._timegrouper._arrow_dtype)
609 return result
611 @final
612 def ffill(self, limit: int | None = None):
613 """
614 Forward fill the values.
616 This method fills missing values by propagating the last valid
617 observation forward, up to the next valid observation. It is commonly
618 used in time series analysis when resampling data to a higher frequency
619 (upsampling) and filling gaps in the resampled output.
621 Parameters
622 ----------
623 limit : int, optional
624 Limit of how many values to fill.
626 Returns
627 -------
628 Series
629 The resampled data with missing values filled forward.
631 See Also
632 --------
633 Series.fillna: Fill NA/NaN values using the specified method.
634 DataFrame.fillna: Fill NA/NaN values using the specified method.
636 Examples
637 --------
638 Here we only create a ``Series``.
640 >>> ser = pd.Series(
641 ... [1, 2, 3, 4],
642 ... index=pd.DatetimeIndex(
643 ... ["2023-01-01", "2023-01-15", "2023-02-01", "2023-02-15"]
644 ... ),
645 ... )
646 >>> ser
647 2023-01-01 1
648 2023-01-15 2
649 2023-02-01 3
650 2023-02-15 4
651 dtype: int64
653 Example for ``ffill`` with downsampling (we have fewer dates after resampling):
655 >>> ser.resample("MS").ffill()
656 2023-01-01 1
657 2023-02-01 3
658 Freq: MS, dtype: int64
660 Example for ``ffill`` with upsampling (fill the new dates with
661 the previous value):
663 >>> ser.resample("W").ffill()
664 2023-01-01 1
665 2023-01-08 1
666 2023-01-15 2
667 2023-01-22 2
668 2023-01-29 2
669 2023-02-05 3
670 2023-02-12 3
671 2023-02-19 4
672 Freq: W-SUN, dtype: int64
674 With upsampling and limiting (only fill the first new date with the
675 previous value):
677 >>> ser.resample("W").ffill(limit=1)
678 2023-01-01 1.0
679 2023-01-08 1.0
680 2023-01-15 2.0
681 2023-01-22 2.0
682 2023-01-29 NaN
683 2023-02-05 3.0
684 2023-02-12 NaN
685 2023-02-19 4.0
686 Freq: W-SUN, dtype: float64
687 """
688 return self._upsample("ffill", limit=limit)
690 @final
691 def nearest(self, limit: int | None = None):
692 """
693 Resample by using the nearest value.
695 When resampling data, missing values may appear (e.g., when the
696 resampling frequency is higher than the original frequency).
697 The `nearest` method will replace ``NaN`` values that appeared in
698 the resampled data with the value from the nearest member of the
699 sequence, based on the index value.
700 Missing values that existed in the original data will not be modified.
701 If `limit` is given, fill only this many values in each direction for
702 each of the original values.
704 Parameters
705 ----------
706 limit : int, optional
707 Limit of how many values to fill.
709 Returns
710 -------
711 Series or DataFrame
712 An upsampled Series or DataFrame with ``NaN`` values filled with
713 their nearest value.
715 See Also
716 --------
717 bfill : Backward fill the new missing values in the resampled data.
718 ffill : Forward fill ``NaN`` values.
720 Examples
721 --------
722 >>> s = pd.Series([1, 2], index=pd.date_range("20180101", periods=2, freq="1h"))
723 >>> s
724 2018-01-01 00:00:00 1
725 2018-01-01 01:00:00 2
726 Freq: h, dtype: int64
728 >>> s.resample("15min").nearest()
729 2018-01-01 00:00:00 1
730 2018-01-01 00:15:00 1
731 2018-01-01 00:30:00 2
732 2018-01-01 00:45:00 2
733 2018-01-01 01:00:00 2
734 Freq: 15min, dtype: int64
736 Limit the number of upsampled values imputed by the nearest:
738 >>> s.resample("15min").nearest(limit=1)
739 2018-01-01 00:00:00 1.0
740 2018-01-01 00:15:00 1.0
741 2018-01-01 00:30:00 NaN
742 2018-01-01 00:45:00 2.0
743 2018-01-01 01:00:00 2.0
744 Freq: 15min, dtype: float64
745 """
746 return self._upsample("nearest", limit=limit)
748 @final
749 def bfill(self, limit: int | None = None):
750 """
751 Backward fill the new missing values in the resampled data.
753 In statistics, imputation is the process of replacing missing data with
754 substituted values [1]_. When resampling data, missing values may
755 appear (e.g., when the resampling frequency is higher than the original
756 frequency). The backward fill will replace NaN values that appeared in
757 the resampled data with the next value in the original sequence.
758 Missing values that existed in the original data will not be modified.
760 Parameters
761 ----------
762 limit : int, optional
763 Limit of how many values to fill.
765 Returns
766 -------
767 Series, DataFrame
768 An upsampled Series or DataFrame with backward filled NaN values.
770 See Also
771 --------
772 nearest : Fill NaN values with nearest neighbor starting from center.
773 ffill : Forward fill NaN values.
774 Series.fillna : Fill NaN values in the Series using the
775 specified method, which can be 'backfill'.
776 DataFrame.fillna : Fill NaN values in the DataFrame using the
777 specified method, which can be 'backfill'.
779 References
780 ----------
781 .. [1] https://en.wikipedia.org/wiki/Imputation_%28statistics%29
783 Examples
784 --------
785 Resampling a Series:
787 >>> s = pd.Series(
788 ... [1, 2, 3], index=pd.date_range("20180101", periods=3, freq="h")
789 ... )
790 >>> s
791 2018-01-01 00:00:00 1
792 2018-01-01 01:00:00 2
793 2018-01-01 02:00:00 3
794 Freq: h, dtype: int64
796 >>> s.resample("30min").bfill()
797 2018-01-01 00:00:00 1
798 2018-01-01 00:30:00 2
799 2018-01-01 01:00:00 2
800 2018-01-01 01:30:00 3
801 2018-01-01 02:00:00 3
802 Freq: 30min, dtype: int64
804 >>> s.resample("15min").bfill(limit=2)
805 2018-01-01 00:00:00 1.0
806 2018-01-01 00:15:00 NaN
807 2018-01-01 00:30:00 2.0
808 2018-01-01 00:45:00 2.0
809 2018-01-01 01:00:00 2.0
810 2018-01-01 01:15:00 NaN
811 2018-01-01 01:30:00 3.0
812 2018-01-01 01:45:00 3.0
813 2018-01-01 02:00:00 3.0
814 Freq: 15min, dtype: float64
816 Resampling a DataFrame that has missing values:
818 >>> df = pd.DataFrame(
819 ... {"a": [2, np.nan, 6], "b": [1, 3, 5]},
820 ... index=pd.date_range("20180101", periods=3, freq="h"),
821 ... )
822 >>> df
823 a b
824 2018-01-01 00:00:00 2.0 1
825 2018-01-01 01:00:00 NaN 3
826 2018-01-01 02:00:00 6.0 5
828 >>> df.resample("30min").bfill()
829 a b
830 2018-01-01 00:00:00 2.0 1
831 2018-01-01 00:30:00 NaN 3
832 2018-01-01 01:00:00 NaN 3
833 2018-01-01 01:30:00 6.0 5
834 2018-01-01 02:00:00 6.0 5
836 >>> df.resample("15min").bfill(limit=2)
837 a b
838 2018-01-01 00:00:00 2.0 1.0
839 2018-01-01 00:15:00 NaN NaN
840 2018-01-01 00:30:00 NaN 3.0
841 2018-01-01 00:45:00 NaN 3.0
842 2018-01-01 01:00:00 NaN 3.0
843 2018-01-01 01:15:00 NaN NaN
844 2018-01-01 01:30:00 6.0 5.0
845 2018-01-01 01:45:00 6.0 5.0
846 2018-01-01 02:00:00 6.0 5.0
847 """
848 return self._upsample("bfill", limit=limit)
850 @final
851 def interpolate(
852 self,
853 method: InterpolateOptions = "linear",
854 *,
855 axis: Axis = 0,
856 limit: int | None = None,
857 limit_direction: Literal["forward", "backward", "both"] = "forward",
858 limit_area=None,
859 **kwargs,
860 ):
861 """
862 Interpolate values between target timestamps according to different methods.
864 The original index is first reindexed to target timestamps
865 (see :meth:`core.resample.Resampler.asfreq`),
866 then the interpolation of ``NaN`` values via :meth:`DataFrame.interpolate`
867 happens.
869 Parameters
870 ----------
871 method : str, default 'linear'
872 Interpolation technique to use. One of:
874 * 'linear': Ignore the index and treat the values as equally
875 spaced. This is the only method supported on MultiIndexes.
876 * 'time': Works on daily and higher resolution data to interpolate
877 given length of interval.
878 * 'index', 'values': use the actual numerical values of the index.
879 * 'pad': Fill in NaNs using existing values.
880 * 'nearest', 'zero', 'slinear', 'quadratic', 'cubic',
881 'barycentric', 'polynomial': Passed to
882 `scipy.interpolate.interp1d`, whereas 'spline' is passed to
883 `scipy.interpolate.UnivariateSpline`. These methods use the numerical
884 values of the index. Both 'polynomial' and 'spline' require that
885 you also specify an `order` (int), e.g.
886 ``df.interpolate(method='polynomial', order=5)``. Note that,
887 `slinear` method in Pandas refers to the Scipy first order `spline`
888 instead of Pandas first order `spline`.
889 * 'krogh', 'piecewise_polynomial', 'spline', 'pchip', 'akima',
890 'cubicspline': Wrappers around the SciPy interpolation methods of
891 similar names. See `Notes`.
892 * 'from_derivatives': Refers to
893 `scipy.interpolate.BPoly.from_derivatives`.
895 axis : {0 or 'index', 1 or 'columns', None}, default None
896 Axis to interpolate along. For `Series` this parameter is unused
897 and defaults to 0.
898 limit : int, optional
899 Maximum number of consecutive NaNs to fill. In other words, if there
900 is a gap with more than this number of consecutive NaNs, it will only
901 be partially filled, from the direction given by ``limit_direction``.
902 Must be greater than 0.
903 limit_direction : {'forward', 'backward', 'both'}, Optional
904 Consecutive NaNs will be filled in this direction.
906 limit_area : {`None`, 'inside', 'outside'}, default None
907 Restrict which NaNs are filled based on their position relative to
908 the valid values.
910 * ``None``: No fill restriction.
911 * 'inside': Only fill NaNs surrounded by valid values
912 (interpolate).
913 * 'outside': Only fill NaNs outside valid values (extrapolate).
915 **kwargs : optional
916 Keyword arguments to pass on to the interpolating function.
918 Returns
919 -------
920 DataFrame or Series
921 Interpolated values at the specified freq.
923 See Also
924 --------
925 core.resample.Resampler.asfreq: Return the values at the new freq,
926 essentially a reindex.
927 DataFrame.interpolate: Fill NaN values using an interpolation method.
928 DataFrame.bfill : Backward fill NaN values in the resampled data.
929 DataFrame.ffill : Forward fill NaN values.
931 Notes
932 -----
933 For high-frequent or non-equidistant time-series with timestamps
934 the reindexing followed by interpolation may lead to information loss
935 as shown in the last example.
937 Examples
938 --------
940 >>> start = "2023-03-01T07:00:00"
941 >>> timesteps = pd.date_range(start, periods=5, freq="s")
942 >>> series = pd.Series(data=[1, -1, 2, 1, 3], index=timesteps)
943 >>> series
944 2023-03-01 07:00:00 1
945 2023-03-01 07:00:01 -1
946 2023-03-01 07:00:02 2
947 2023-03-01 07:00:03 1
948 2023-03-01 07:00:04 3
949 Freq: s, dtype: int64
951 Downsample the dataframe to 0.5Hz by providing the period time of 2s.
953 >>> series.resample("2s").interpolate("linear")
954 2023-03-01 07:00:00 1
955 2023-03-01 07:00:02 2
956 2023-03-01 07:00:04 3
957 Freq: 2s, dtype: int64
959 Upsample the dataframe to 2Hz by providing the period time of 500ms.
961 >>> series.resample("500ms").interpolate("linear")
962 2023-03-01 07:00:00.000 1.0
963 2023-03-01 07:00:00.500 0.0
964 2023-03-01 07:00:01.000 -1.0
965 2023-03-01 07:00:01.500 0.5
966 2023-03-01 07:00:02.000 2.0
967 2023-03-01 07:00:02.500 1.5
968 2023-03-01 07:00:03.000 1.0
969 2023-03-01 07:00:03.500 2.0
970 2023-03-01 07:00:04.000 3.0
971 Freq: 500ms, dtype: float64
973 Internal reindexing with ``asfreq()`` prior to interpolation leads to
974 an interpolated timeseries on the basis of the reindexed timestamps
975 (anchors). It is assured that all available datapoints from original
976 series become anchors, so it also works for resampling-cases that lead
977 to non-aligned timestamps, as in the following example:
979 >>> series.resample("400ms").interpolate("linear")
980 2023-03-01 07:00:00.000 1.000000
981 2023-03-01 07:00:00.400 0.333333
982 2023-03-01 07:00:00.800 -0.333333
983 2023-03-01 07:00:01.200 0.000000
984 2023-03-01 07:00:01.600 1.000000
985 2023-03-01 07:00:02.000 2.000000
986 2023-03-01 07:00:02.400 1.666667
987 2023-03-01 07:00:02.800 1.333333
988 2023-03-01 07:00:03.200 1.666667
989 2023-03-01 07:00:03.600 2.333333
990 2023-03-01 07:00:04.000 3.000000
991 Freq: 400ms, dtype: float64
993 Note that the series correctly decreases between two anchors
994 ``07:00:00`` and ``07:00:02``.
995 """
996 if "inplace" in kwargs:
997 # GH#58690
998 warnings.warn(
999 f"The 'inplace' keyword in {type(self).__name__}.interpolate "
1000 "is deprecated and will be removed in a future version. "
1001 "resample(...).interpolate is never inplace.",
1002 Pandas4Warning,
1003 stacklevel=find_stack_level(),
1004 )
1005 inplace = kwargs.pop("inplace")
1006 if inplace:
1007 raise ValueError("Cannot interpolate inplace on a resampled object.")
1009 result = self._upsample("asfreq")
1011 # If the original data has timestamps which are not aligned with the
1012 # target timestamps, we need to add those points back to the data frame
1013 # that is supposed to be interpolated. This does not work with
1014 # PeriodIndex, so we skip this case. GH#21351
1015 obj = self._selected_obj
1016 is_period_index = isinstance(obj.index, PeriodIndex)
1018 # Skip this step for PeriodIndex
1019 if not is_period_index:
1020 final_index = result.index
1021 if isinstance(final_index, MultiIndex):
1022 raise NotImplementedError(
1023 "Direct interpolation of MultiIndex data frames is not "
1024 "supported. If you tried to resample and interpolate on a "
1025 "grouped data frame, please use:\n"
1026 "`df.groupby(...).apply(lambda x: x.resample(...)."
1027 "interpolate(...))`"
1028 "\ninstead, as resampling and interpolation has to be "
1029 "performed for each group independently."
1030 )
1032 missing_data_points_index = obj.index.difference(final_index)
1033 if len(missing_data_points_index) > 0:
1034 result = concat(
1035 [result, obj.loc[missing_data_points_index]]
1036 ).sort_index()
1038 result_interpolated = result.interpolate(
1039 method=method,
1040 axis=axis,
1041 limit=limit,
1042 inplace=False,
1043 limit_direction=limit_direction,
1044 limit_area=limit_area,
1045 **kwargs,
1046 )
1048 # No further steps if the original data has a PeriodIndex
1049 if is_period_index:
1050 return result_interpolated
1052 # Make sure that original data points which do not align with the
1053 # resampled index are removed
1054 result_interpolated = result_interpolated.loc[final_index]
1056 # Make sure frequency indexes are preserved
1057 result_interpolated.index = final_index
1058 return result_interpolated
1060 @final
1061 def asfreq(self, fill_value=None):
1062 """
1063 Return the values at the new freq, essentially a reindex.
1065 This method selects data at the specified frequency without aggregation,
1066 useful for upsampling to a higher frequency or selecting specific time points.
1068 Parameters
1069 ----------
1070 fill_value : scalar, optional
1071 Value to use for missing values, applied during upsampling (note
1072 this does not fill NaNs that already were present).
1074 Returns
1075 -------
1076 DataFrame or Series
1077 Values at the specified freq.
1079 See Also
1080 --------
1081 Series.asfreq: Convert TimeSeries to specified frequency.
1082 DataFrame.asfreq: Convert TimeSeries to specified frequency.
1084 Examples
1085 --------
1087 >>> ser = pd.Series(
1088 ... [1, 2, 3, 4],
1089 ... index=pd.DatetimeIndex(
1090 ... ["2023-01-01", "2023-01-31", "2023-02-01", "2023-02-28"]
1091 ... ),
1092 ... )
1093 >>> ser
1094 2023-01-01 1
1095 2023-01-31 2
1096 2023-02-01 3
1097 2023-02-28 4
1098 dtype: int64
1099 >>> ser.resample("MS").asfreq()
1100 2023-01-01 1
1101 2023-02-01 3
1102 Freq: MS, dtype: int64
1103 """
1104 return self._upsample("asfreq", fill_value=fill_value)
1106 @final
1107 def sum(
1108 self,
1109 numeric_only: bool = False,
1110 min_count: int = 0,
1111 ):
1112 """
1113 Compute sum of group values.
1115 This method provides a simple way to compute the sum of values within each
1116 resampled group, particularly useful for aggregating time-based data into
1117 daily, monthly, or yearly sums.
1119 Parameters
1120 ----------
1121 numeric_only : bool, default False
1122 Include only float, int, boolean columns.
1124 .. versionchanged:: 2.0.0
1126 numeric_only no longer accepts ``None``.
1128 min_count : int, default 0
1129 The required number of valid values to perform the operation. If fewer
1130 than ``min_count`` non-NA values are present the result will be NA.
1132 Returns
1133 -------
1134 Series or DataFrame
1135 Computed sum of values within each group.
1137 See Also
1138 --------
1139 core.resample.Resampler.mean : Compute mean of groups, excluding missing values.
1140 core.resample.Resampler.count : Compute count of group, excluding missing
1141 values.
1142 DataFrame.resample : Resample time-series data.
1143 Series.sum : Return the sum of the values over the requested axis.
1145 Examples
1146 --------
1147 >>> ser = pd.Series(
1148 ... [1, 2, 3, 4],
1149 ... index=pd.DatetimeIndex(
1150 ... ["2023-01-01", "2023-01-15", "2023-02-01", "2023-02-15"]
1151 ... ),
1152 ... )
1153 >>> ser
1154 2023-01-01 1
1155 2023-01-15 2
1156 2023-02-01 3
1157 2023-02-15 4
1158 dtype: int64
1159 >>> ser.resample("MS").sum()
1160 2023-01-01 3
1161 2023-02-01 7
1162 Freq: MS, dtype: int64
1163 """
1164 return self._downsample("sum", numeric_only=numeric_only, min_count=min_count)
1166 @final
1167 def prod(
1168 self,
1169 numeric_only: bool = False,
1170 min_count: int = 0,
1171 ):
1172 """
1173 Compute prod of group values.
1175 This method calculates the product of all values within each resampled
1176 time period.
1178 Parameters
1179 ----------
1180 numeric_only : bool, default False
1181 Include only float, int, boolean columns.
1183 .. versionchanged:: 2.0.0
1185 numeric_only no longer accepts ``None``.
1187 min_count : int, default 0
1188 The required number of valid values to perform the operation. If fewer
1189 than ``min_count`` non-NA values are present the result will be NA.
1191 Returns
1192 -------
1193 Series or DataFrame
1194 Computed prod of values within each group.
1196 See Also
1197 --------
1198 core.resample.Resampler.sum : Compute sum of groups, excluding missing values.
1199 core.resample.Resampler.mean : Compute mean of groups, excluding missing values.
1200 core.resample.Resampler.median : Compute median of groups, excluding missing
1201 values.
1203 Examples
1204 --------
1205 >>> ser = pd.Series(
1206 ... [1, 2, 3, 4],
1207 ... index=pd.DatetimeIndex(
1208 ... ["2023-01-01", "2023-01-15", "2023-02-01", "2023-02-15"]
1209 ... ),
1210 ... )
1211 >>> ser
1212 2023-01-01 1
1213 2023-01-15 2
1214 2023-02-01 3
1215 2023-02-15 4
1216 dtype: int64
1217 >>> ser.resample("MS").prod()
1218 2023-01-01 2
1219 2023-02-01 12
1220 Freq: MS, dtype: int64
1221 """
1222 return self._downsample("prod", numeric_only=numeric_only, min_count=min_count)
1224 @final
1225 def min(
1226 self,
1227 numeric_only: bool = False,
1228 min_count: int = 0,
1229 ):
1230 """
1231 Compute min value of group.
1233 This method returns the minimum value within each resampled time period.
1235 Parameters
1236 ----------
1237 numeric_only : bool, default False
1238 Include only float, int, boolean columns.
1240 .. versionchanged:: 2.0.0
1242 numeric_only no longer accepts ``None``.
1244 min_count : int, default 0
1245 The required number of valid values to perform the operation. If fewer
1246 than ``min_count`` non-NA values are present the result will be NA.
1248 Returns
1249 -------
1250 Series or DataFrame
1251 Compute the minimum value in the given Series or DataFrame.
1253 See Also
1254 --------
1255 core.resample.Resampler.max : Compute max value of group.
1256 core.resample.Resampler.mean : Compute mean of groups, excluding missing values.
1257 core.resample.Resampler.median : Compute median of groups, excluding missing
1258 values.
1260 Examples
1261 --------
1262 >>> ser = pd.Series(
1263 ... [1, 2, 3, 4],
1264 ... index=pd.DatetimeIndex(
1265 ... ["2023-01-01", "2023-01-15", "2023-02-01", "2023-02-15"]
1266 ... ),
1267 ... )
1268 >>> ser
1269 2023-01-01 1
1270 2023-01-15 2
1271 2023-02-01 3
1272 2023-02-15 4
1273 dtype: int64
1274 >>> ser.resample("MS").min()
1275 2023-01-01 1
1276 2023-02-01 3
1277 Freq: MS, dtype: int64
1278 """
1279 return self._downsample("min", numeric_only=numeric_only, min_count=min_count)
1281 @final
1282 def max(
1283 self,
1284 numeric_only: bool = False,
1285 min_count: int = 0,
1286 ):
1287 """
1288 Compute max value of group.
1290 This method returns the maximum value within each resampled time period.
1292 Parameters
1293 ----------
1294 numeric_only : bool, default False
1295 Include only float, int, boolean columns.
1297 .. versionchanged:: 2.0.0
1299 numeric_only no longer accepts ``None``.
1301 min_count : int, default 0
1302 The required number of valid values to perform the operation. If fewer
1303 than ``min_count`` non-NA values are present the result will be NA.
1305 Returns
1306 -------
1307 Series or DataFrame
1308 Computes the maximum value in the given Series or Dataframe.
1310 See Also
1311 --------
1312 core.resample.Resampler.min : Compute min value of group.
1313 core.resample.Resampler.mean : Compute mean of groups, excluding missing values.
1314 core.resample.Resampler.median : Compute median of groups, excluding missing
1315 values.
1317 Examples
1318 --------
1319 >>> ser = pd.Series(
1320 ... [1, 2, 3, 4],
1321 ... index=pd.DatetimeIndex(
1322 ... ["2023-01-01", "2023-01-15", "2023-02-01", "2023-02-15"]
1323 ... ),
1324 ... )
1325 >>> ser
1326 2023-01-01 1
1327 2023-01-15 2
1328 2023-02-01 3
1329 2023-02-15 4
1330 dtype: int64
1331 >>> ser.resample("MS").max()
1332 2023-01-01 2
1333 2023-02-01 4
1334 Freq: MS, dtype: int64
1335 """
1336 return self._downsample("max", numeric_only=numeric_only, min_count=min_count)
1338 @final
1339 def first(
1340 self,
1341 numeric_only: bool = False,
1342 min_count: int = 0,
1343 skipna: bool = True,
1344 ):
1345 """
1346 Compute the first non-null entry of each column.
1348 This method returns the first value encountered in each resampled time
1349 period, skipping missing values by default.
1351 Parameters
1352 ----------
1353 numeric_only : bool, default False
1354 Include only float, int, boolean columns.
1355 min_count : int, default 0
1356 The required number of valid values to perform the operation. If fewer
1357 than ``min_count`` non-NA values are present the result will be NA.
1358 skipna : bool, default True
1359 Exclude NA/null values. If an entire group is NA, the result will be NA.
1361 Returns
1362 -------
1363 Series or DataFrame
1364 First values within each group.
1366 See Also
1367 --------
1368 core.resample.Resampler.last : Compute the last non-null value in each group.
1369 core.resample.Resampler.mean : Compute mean of groups, excluding missing values.
1371 Examples
1372 --------
1373 >>> s = pd.Series(
1374 ... [1, 2, 3, 4],
1375 ... index=pd.DatetimeIndex(
1376 ... ["2023-01-01", "2023-01-15", "2023-02-01", "2023-02-15"]
1377 ... ),
1378 ... )
1379 >>> s
1380 2023-01-01 1
1381 2023-01-15 2
1382 2023-02-01 3
1383 2023-02-15 4
1384 dtype: int64
1385 >>> s.resample("MS").first()
1386 2023-01-01 1
1387 2023-02-01 3
1388 Freq: MS, dtype: int64
1389 """
1390 return self._downsample(
1391 "first", numeric_only=numeric_only, min_count=min_count, skipna=skipna
1392 )
1394 @final
1395 def last(
1396 self,
1397 numeric_only: bool = False,
1398 min_count: int = 0,
1399 skipna: bool = True,
1400 ):
1401 """
1402 Compute the last non-null entry of each column.
1404 This method returns the last value encountered in each resampled time
1405 period, skipping missing values by default.
1407 Parameters
1408 ----------
1409 numeric_only : bool, default False
1410 Include only float, int, boolean columns.
1411 min_count : int, default 0
1412 The required number of valid values to perform the operation. If fewer
1413 than ``min_count`` non-NA values are present the result will be NA.
1414 skipna : bool, default True
1415 Exclude NA/null values. If an entire group is NA, the result will be NA.
1417 Returns
1418 -------
1419 Series or DataFrame
1420 Last of values within each group.
1422 See Also
1423 --------
1424 core.resample.Resampler.first : Compute the first non-null value in each group.
1425 core.resample.Resampler.mean : Compute mean of groups, excluding missing values.
1427 Examples
1428 --------
1429 >>> s = pd.Series(
1430 ... [1, 2, 3, 4],
1431 ... index=pd.DatetimeIndex(
1432 ... ["2023-01-01", "2023-01-15", "2023-02-01", "2023-02-15"]
1433 ... ),
1434 ... )
1435 >>> s
1436 2023-01-01 1
1437 2023-01-15 2
1438 2023-02-01 3
1439 2023-02-15 4
1440 dtype: int64
1441 >>> s.resample("MS").last()
1442 2023-01-01 2
1443 2023-02-01 4
1444 Freq: MS, dtype: int64
1445 """
1446 return self._downsample(
1447 "last", numeric_only=numeric_only, min_count=min_count, skipna=skipna
1448 )
1450 @final
1451 def median(self, numeric_only: bool = False):
1452 """
1453 Compute median of groups, excluding missing values.
1455 For multiple groupings, the result index will be a MultiIndex
1457 Parameters
1458 ----------
1459 numeric_only : bool, default False
1460 Include only float, int, boolean columns.
1462 .. versionchanged:: 2.0.0
1464 numeric_only no longer accepts ``None`` and defaults to False.
1466 Returns
1467 -------
1468 Series or DataFrame
1469 Median of values within each group.
1471 See Also
1472 --------
1473 Series.groupby : Apply a function groupby to a Series.
1474 DataFrame.groupby : Apply a function groupby to each row or column of a
1475 DataFrame.
1477 Examples
1478 --------
1480 >>> ser = pd.Series(
1481 ... [1, 2, 3, 3, 4, 5],
1482 ... index=pd.DatetimeIndex(
1483 ... [
1484 ... "2023-01-01",
1485 ... "2023-01-10",
1486 ... "2023-01-15",
1487 ... "2023-02-01",
1488 ... "2023-02-10",
1489 ... "2023-02-15",
1490 ... ]
1491 ... ),
1492 ... )
1493 >>> ser.resample("MS").median()
1494 2023-01-01 2.0
1495 2023-02-01 4.0
1496 Freq: MS, dtype: float64
1497 """
1498 return self._downsample("median", numeric_only=numeric_only)
1500 @final
1501 def mean(
1502 self,
1503 numeric_only: bool = False,
1504 ):
1505 """
1506 Compute mean of groups, excluding missing values.
1508 This method calculates the arithmetic mean of values within each
1509 resampled time period.
1511 Parameters
1512 ----------
1513 numeric_only : bool, default False
1514 Include only `float`, `int` or `boolean` data.
1516 .. versionchanged:: 2.0.0
1518 numeric_only now defaults to ``False``.
1520 Returns
1521 -------
1522 DataFrame or Series
1523 Mean of values within each group.
1525 See Also
1526 --------
1527 core.resample.Resampler.median : Compute median of groups, excluding missing
1528 values.
1529 core.resample.Resampler.sum : Compute sum of groups, excluding missing values.
1530 core.resample.Resampler.std : Compute standard deviation of groups, excluding
1531 missing values.
1532 core.resample.Resampler.var : Compute variance of groups, excluding missing
1533 values.
1535 Examples
1536 --------
1538 >>> ser = pd.Series(
1539 ... [1, 2, 3, 4],
1540 ... index=pd.DatetimeIndex(
1541 ... ["2023-01-01", "2023-01-15", "2023-02-01", "2023-02-15"]
1542 ... ),
1543 ... )
1544 >>> ser
1545 2023-01-01 1
1546 2023-01-15 2
1547 2023-02-01 3
1548 2023-02-15 4
1549 dtype: int64
1550 >>> ser.resample("MS").mean()
1551 2023-01-01 1.5
1552 2023-02-01 3.5
1553 Freq: MS, dtype: float64
1554 """
1555 return self._downsample("mean", numeric_only=numeric_only)
1557 @final
1558 def std(
1559 self,
1560 ddof: int = 1,
1561 numeric_only: bool = False,
1562 ):
1563 """
1564 Compute standard deviation of groups, excluding missing values.
1566 This method calculates the sample standard deviation of values within
1567 each resampled time period.
1569 Parameters
1570 ----------
1571 ddof : int, default 1
1572 Degrees of freedom.
1573 numeric_only : bool, default False
1574 Include only `float`, `int` or `boolean` data.
1576 .. versionchanged:: 2.0.0
1578 numeric_only now defaults to ``False``.
1580 Returns
1581 -------
1582 DataFrame or Series
1583 Standard deviation of values within each group.
1585 See Also
1586 --------
1587 core.resample.Resampler.mean : Compute mean of groups, excluding missing values.
1588 core.resample.Resampler.median : Compute median of groups, excluding missing
1589 values.
1590 core.resample.Resampler.var : Compute variance of groups, excluding missing
1591 values.
1593 Notes
1594 -----
1595 To use the same divisor as ``numpy.std``, use ``ddof=0`` instead of
1596 the default ``ddof=1``.
1598 Examples
1599 --------
1601 >>> ser = pd.Series(
1602 ... [1, 3, 2, 4, 3, 8],
1603 ... index=pd.DatetimeIndex(
1604 ... [
1605 ... "2023-01-01",
1606 ... "2023-01-10",
1607 ... "2023-01-15",
1608 ... "2023-02-01",
1609 ... "2023-02-10",
1610 ... "2023-02-15",
1611 ... ]
1612 ... ),
1613 ... )
1614 >>> ser.resample("MS").std()
1615 2023-01-01 1.000000
1616 2023-02-01 2.645751
1617 Freq: MS, dtype: float64
1618 """
1619 return self._downsample("std", ddof=ddof, numeric_only=numeric_only)
1621 @final
1622 def var(
1623 self,
1624 ddof: int = 1,
1625 numeric_only: bool = False,
1626 ):
1627 """
1628 Compute variance of groups, excluding missing values.
1630 This method calculates the sample variance of values within each
1631 resampled time period.
1633 Parameters
1634 ----------
1635 ddof : int, default 1
1636 Degrees of freedom.
1638 numeric_only : bool, default False
1639 Include only `float`, `int` or `boolean` data.
1641 .. versionchanged:: 2.0.0
1643 numeric_only now defaults to ``False``.
1645 Returns
1646 -------
1647 DataFrame or Series
1648 Variance of values within each group.
1650 See Also
1651 --------
1652 core.resample.Resampler.std : Compute standard deviation of groups, excluding
1653 missing values.
1654 core.resample.Resampler.mean : Compute mean of groups, excluding missing values.
1655 core.resample.Resampler.median : Compute median of groups, excluding missing
1656 values.
1658 Examples
1659 --------
1661 >>> ser = pd.Series(
1662 ... [1, 3, 2, 4, 3, 8],
1663 ... index=pd.DatetimeIndex(
1664 ... [
1665 ... "2023-01-01",
1666 ... "2023-01-10",
1667 ... "2023-01-15",
1668 ... "2023-02-01",
1669 ... "2023-02-10",
1670 ... "2023-02-15",
1671 ... ]
1672 ... ),
1673 ... )
1674 >>> ser.resample("MS").var()
1675 2023-01-01 1.0
1676 2023-02-01 7.0
1677 Freq: MS, dtype: float64
1679 >>> ser.resample("MS").var(ddof=0)
1680 2023-01-01 0.666667
1681 2023-02-01 4.666667
1682 Freq: MS, dtype: float64
1683 """
1684 return self._downsample("var", ddof=ddof, numeric_only=numeric_only)
1686 @final
1687 def sem(
1688 self,
1689 ddof: int = 1,
1690 numeric_only: bool = False,
1691 ):
1692 """
1693 Compute standard error of the mean of groups, excluding missing values.
1695 For multiple groupings, the result index will be a MultiIndex.
1697 Parameters
1698 ----------
1699 ddof : int, default 1
1700 Degrees of freedom.
1702 numeric_only : bool, default False
1703 Include only `float`, `int` or `boolean` data.
1705 .. versionchanged:: 2.0.0
1707 numeric_only now defaults to ``False``.
1709 Returns
1710 -------
1711 Series or DataFrame
1712 Standard error of the mean of values within each group.
1714 See Also
1715 --------
1716 DataFrame.sem : Return unbiased standard error of the mean over requested axis.
1717 Series.sem : Return unbiased standard error of the mean over requested axis.
1719 Examples
1720 --------
1722 >>> ser = pd.Series(
1723 ... [1, 3, 2, 4, 3, 8],
1724 ... index=pd.DatetimeIndex(
1725 ... [
1726 ... "2023-01-01",
1727 ... "2023-01-10",
1728 ... "2023-01-15",
1729 ... "2023-02-01",
1730 ... "2023-02-10",
1731 ... "2023-02-15",
1732 ... ]
1733 ... ),
1734 ... )
1735 >>> ser.resample("MS").sem()
1736 2023-01-01 0.577350
1737 2023-02-01 1.527525
1738 Freq: MS, dtype: float64
1739 """
1740 return self._downsample("sem", ddof=ddof, numeric_only=numeric_only)
1742 @final
1743 def ohlc(self):
1744 """
1745 Compute open, high, low and close values of a group, excluding missing values.
1747 This method computes OHLC (Open-High-Low-Close) values, commonly used
1748 in financial data analysis to summarize price movements within time periods.
1750 Returns
1751 -------
1752 DataFrame
1753 Open, high, low and close values within each group.
1755 See Also
1756 --------
1757 DataFrame.agg : Aggregate using one or more operations over the specified axis.
1758 DataFrame.resample : Resample time-series data.
1759 DataFrame.groupby : Group DataFrame using a mapper or by a Series of columns.
1761 Examples
1762 --------
1763 >>> ser = pd.Series(
1764 ... [1, 3, 2, 4, 3, 5],
1765 ... index=pd.DatetimeIndex(
1766 ... [
1767 ... "2023-01-01",
1768 ... "2023-01-10",
1769 ... "2023-01-15",
1770 ... "2023-02-01",
1771 ... "2023-02-10",
1772 ... "2023-02-15",
1773 ... ]
1774 ... ),
1775 ... )
1776 >>> ser.resample("MS").ohlc()
1777 open high low close
1778 2023-01-01 1 3 1 2
1779 2023-02-01 4 5 3 5
1780 """
1781 ax = self.ax
1782 obj = self._obj_with_exclusions
1783 if len(ax) == 0:
1784 # GH#42902
1785 obj = obj.copy()
1786 obj.index = _asfreq_compat(obj.index, self.freq)
1787 if obj.ndim == 1:
1788 obj = obj.to_frame()
1789 obj = obj.reindex(["open", "high", "low", "close"], axis=1)
1790 else:
1791 mi = MultiIndex.from_product(
1792 [obj.columns, ["open", "high", "low", "close"]]
1793 )
1794 obj = obj.reindex(mi, axis=1)
1795 return obj
1797 return self._downsample("ohlc")
1799 @final
1800 def nunique(self):
1801 """
1802 Return number of unique elements in the group.
1804 This method counts the number of distinct values within each resampled
1805 time period.
1807 Returns
1808 -------
1809 Series
1810 Number of unique values within each group.
1812 See Also
1813 --------
1814 core.groupby.SeriesGroupBy.nunique : Method nunique for SeriesGroupBy.
1816 Examples
1817 --------
1818 >>> ser = pd.Series(
1819 ... [1, 2, 3, 3],
1820 ... index=pd.DatetimeIndex(
1821 ... ["2023-01-01", "2023-01-15", "2023-02-01", "2023-02-15"]
1822 ... ),
1823 ... )
1824 >>> ser
1825 2023-01-01 1
1826 2023-01-15 2
1827 2023-02-01 3
1828 2023-02-15 3
1829 dtype: int64
1830 >>> ser.resample("MS").nunique()
1831 2023-01-01 2
1832 2023-02-01 1
1833 Freq: MS, dtype: int64
1834 """
1835 return self._downsample("nunique")
1837 @final
1838 def size(self):
1839 """
1840 Compute group sizes.
1842 This method returns the number of rows in each resampled time period,
1843 including rows with missing values.
1845 Returns
1846 -------
1847 Series
1848 Number of rows in each group.
1850 See Also
1851 --------
1852 Series.groupby : Apply a function groupby to a Series.
1853 DataFrame.groupby : Apply a function groupby to each row
1854 or column of a DataFrame.
1856 Examples
1857 --------
1858 >>> ser = pd.Series(
1859 ... [1, 2, 3],
1860 ... index=pd.DatetimeIndex(["2023-01-01", "2023-01-15", "2023-02-01"]),
1861 ... )
1862 >>> ser
1863 2023-01-01 1
1864 2023-01-15 2
1865 2023-02-01 3
1866 dtype: int64
1867 >>> ser.resample("MS").size()
1868 2023-01-01 2
1869 2023-02-01 1
1870 Freq: MS, dtype: int64
1871 """
1872 result = self._downsample("size")
1874 # If the result is a non-empty DataFrame we stack to get a Series
1875 # GH 46826
1876 if isinstance(result, ABCDataFrame) and not result.empty:
1877 result = result.stack()
1879 if not len(self.ax):
1880 from pandas import Series
1882 if self._selected_obj.ndim == 1:
1883 name = self._selected_obj.name
1884 else:
1885 name = None
1886 result = Series([], index=result.index, dtype="int64", name=name)
1887 return result
1889 @final
1890 def count(self):
1891 """
1892 Compute count of group, excluding missing values.
1894 This method returns the number of non-null values in each resampled
1895 time period.
1897 Returns
1898 -------
1899 Series or DataFrame
1900 Count of values within each group.
1902 See Also
1903 --------
1904 Series.groupby : Apply a function groupby to a Series.
1905 DataFrame.groupby : Apply a function groupby to each row
1906 or column of a DataFrame.
1908 Examples
1909 --------
1910 >>> ser = pd.Series(
1911 ... [1, 2, 3, 4],
1912 ... index=pd.DatetimeIndex(
1913 ... ["2023-01-01", "2023-01-15", "2023-02-01", "2023-02-15"]
1914 ... ),
1915 ... )
1916 >>> ser
1917 2023-01-01 1
1918 2023-01-15 2
1919 2023-02-01 3
1920 2023-02-15 4
1921 dtype: int64
1922 >>> ser.resample("MS").count()
1923 2023-01-01 2
1924 2023-02-01 2
1925 Freq: MS, dtype: int64
1926 """
1927 result = self._downsample("count")
1928 if not len(self.ax):
1929 if self._selected_obj.ndim == 1:
1930 result = type(self._selected_obj)(
1931 [], index=result.index, dtype="int64", name=self._selected_obj.name
1932 )
1933 else:
1934 from pandas import DataFrame
1936 result = DataFrame(
1937 [], index=result.index, columns=result.columns, dtype="int64"
1938 )
1940 return result
1942 @final
1943 def quantile(self, q: float | list[float] | AnyArrayLike = 0.5, **kwargs):
1944 """
1945 Return value at the given quantile.
1947 Computes the quantile of values within each resampled group.
1949 Parameters
1950 ----------
1951 q : float or array-like, default 0.5 (50% quantile)
1952 Value between 0 <= q <= 1, the quantile(s) to compute.
1953 **kwargs
1954 Additional keyword arguments to be passed to the function.
1956 Returns
1957 -------
1958 DataFrame or Series
1959 Quantile of values within each group.
1961 See Also
1962 --------
1963 Series.quantile
1964 Return a series, where the index is q and the values are the quantiles.
1965 DataFrame.quantile
1966 Return a DataFrame, where the columns are the columns of self,
1967 and the values are the quantiles.
1968 DataFrameGroupBy.quantile
1969 Return a DataFrame, where the columns are groupby columns,
1970 and the values are its quantiles.
1972 Examples
1973 --------
1975 >>> ser = pd.Series(
1976 ... [1, 3, 2, 4, 3, 8],
1977 ... index=pd.DatetimeIndex(
1978 ... [
1979 ... "2023-01-01",
1980 ... "2023-01-10",
1981 ... "2023-01-15",
1982 ... "2023-02-01",
1983 ... "2023-02-10",
1984 ... "2023-02-15",
1985 ... ]
1986 ... ),
1987 ... )
1988 >>> ser.resample("MS").quantile()
1989 2023-01-01 2.0
1990 2023-02-01 4.0
1991 Freq: MS, dtype: float64
1993 >>> ser.resample("MS").quantile(0.25)
1994 2023-01-01 1.5
1995 2023-02-01 3.5
1996 Freq: MS, dtype: float64
1997 """
1998 return self._downsample("quantile", q=q, **kwargs)
2001class _GroupByMixin(PandasObject, SelectionMixin):
2002 """
2003 Provide the groupby facilities.
2004 """
2006 _attributes: list[str] # in practice the same as Resampler._attributes
2007 _selection: IndexLabel | None = None
2008 _groupby: GroupBy
2009 _timegrouper: TimeGrouper
2011 def __init__(
2012 self,
2013 *,
2014 parent: Resampler,
2015 groupby: GroupBy,
2016 key=None,
2017 selection: IndexLabel | None = None,
2018 ) -> None:
2019 # reached via ._gotitem and _get_resampler_for_grouping
2021 assert isinstance(groupby, GroupBy), type(groupby)
2023 # parent is always a Resampler, sometimes a _GroupByMixin
2024 assert isinstance(parent, Resampler), type(parent)
2026 # initialize our GroupByMixin object with
2027 # the resampler attributes
2028 for attr in self._attributes:
2029 setattr(self, attr, getattr(parent, attr))
2030 self._selection = selection
2032 self.binner = parent.binner
2033 self.key = key
2035 self._groupby = groupby
2036 self._timegrouper = copy.copy(parent._timegrouper)
2038 self.ax = parent.ax
2039 self.obj = parent.obj
2041 @no_type_check
2042 def _apply(self, f, *args, **kwargs):
2043 """
2044 Dispatch to _upsample; we are stripping all of the _upsample kwargs and
2045 performing the original function call on the grouped object.
2046 """
2048 def func(x):
2049 x = self._resampler_cls(x, timegrouper=self._timegrouper, gpr_index=self.ax)
2051 if isinstance(f, str):
2052 return getattr(x, f)(**kwargs)
2054 return x.apply(f, *args, **kwargs)
2056 result = self._groupby.apply(func)
2058 # GH 47705
2059 if (
2060 isinstance(result, ABCDataFrame)
2061 and len(result) == 0
2062 and not isinstance(result.index, PeriodIndex)
2063 ):
2064 result = result.set_index(
2065 _asfreq_compat(self.obj.index[:0], freq=self.freq), append=True
2066 )
2068 return self._wrap_result(result)
2070 _upsample = _apply
2071 _downsample = _apply
2072 _groupby_and_aggregate = _apply
2074 @final
2075 def _gotitem(self, key, ndim, subset=None):
2076 """
2077 Sub-classes to define. Return a sliced object.
2079 Parameters
2080 ----------
2081 key : string / list of selections
2082 ndim : {1, 2}
2083 requested ndim of result
2084 subset : object, default None
2085 subset to act on
2086 """
2087 # create a new object to prevent aliasing
2088 if subset is None:
2089 subset = self.obj
2090 if key is not None:
2091 subset = subset[key]
2092 else:
2093 # reached via Apply.agg_dict_like with selection=None, ndim=1
2094 assert subset.ndim == 1
2096 # Try to select from a DataFrame, falling back to a Series
2097 try:
2098 if isinstance(key, list) and self.key not in key and self.key is not None:
2099 key.append(self.key)
2100 groupby = self._groupby[key]
2101 except IndexError:
2102 groupby = self._groupby
2104 selection = self._infer_selection(key, subset)
2106 new_rs = type(self)(
2107 groupby=groupby,
2108 parent=cast("Resampler", self),
2109 selection=selection,
2110 )
2111 return new_rs
2114class DatetimeIndexResampler(Resampler):
2115 ax: DatetimeIndex
2117 @property
2118 def _resampler_for_grouping(self) -> type[DatetimeIndexResamplerGroupBy]:
2119 return DatetimeIndexResamplerGroupBy
2121 def _get_binner_for_time(self):
2122 # this is how we are actually creating the bins
2123 return self._timegrouper._get_time_bins(self.ax)
2125 def _downsample(self, how, **kwargs):
2126 """
2127 Downsample the cython defined function.
2129 Parameters
2130 ----------
2131 how : string / cython mapped function
2132 **kwargs : kw args passed to how function
2133 """
2134 ax = self.ax
2136 # Excludes `on` column when provided
2137 obj = self._obj_with_exclusions
2139 if not len(ax):
2140 # reset to the new freq
2141 obj = obj.copy()
2142 obj.index = obj.index._with_freq(self.freq)
2143 assert obj.index.freq == self.freq, (obj.index.freq, self.freq)
2144 return obj
2146 # we are downsampling
2147 # we want to call the actual grouper method here
2148 result = obj.groupby(self._grouper).aggregate(how, **kwargs)
2149 return self._wrap_result(result)
2151 def _adjust_binner_for_upsample(self, binner):
2152 """
2153 Adjust our binner when upsampling.
2155 The range of a new index should not be outside specified range
2156 """
2157 if self.closed == "right":
2158 binner = binner[1:]
2159 else:
2160 binner = binner[:-1]
2161 return binner
2163 def _upsample(self, method, limit: int | None = None, fill_value=None):
2164 """
2165 Parameters
2166 ----------
2167 method : string {'backfill', 'bfill', 'pad',
2168 'ffill', 'asfreq'} method for upsampling
2169 limit : int, default None
2170 Maximum size gap to fill when reindexing
2171 fill_value : scalar, default None
2172 Value to use for missing values
2173 """
2174 if self._from_selection:
2175 raise ValueError(
2176 "Upsampling from level= or on= selection "
2177 "is not supported, use .set_index(...) "
2178 "to explicitly set index to datetime-like"
2179 )
2181 ax = self.ax
2182 obj = self._selected_obj
2183 binner = self.binner
2184 res_index = self._adjust_binner_for_upsample(binner)
2186 # if index exactly matches target grid (same freq & alignment), use fast path
2187 if (
2188 limit is None
2189 and to_offset(ax._inferred_freq_str) == self.freq
2190 and len(obj) == len(res_index)
2191 and obj.index.equals(res_index)
2192 ):
2193 result = obj.copy()
2194 result.index = res_index
2195 else:
2196 if method == "asfreq":
2197 method = None
2198 result = obj.reindex(
2199 res_index, method=method, limit=limit, fill_value=fill_value
2200 )
2202 return self._wrap_result(result)
2204 def _wrap_result(self, result):
2205 result = super()._wrap_result(result)
2207 # we may have a different kind that we were asked originally
2208 # convert if needed
2209 if isinstance(self.ax, PeriodIndex) and not isinstance(
2210 result.index, PeriodIndex
2211 ):
2212 if isinstance(result.index, MultiIndex):
2213 # GH 24103 - e.g. groupby resample
2214 if not isinstance(result.index.levels[-1], PeriodIndex):
2215 new_level = result.index.levels[-1].to_period(self.freq)
2216 result.index = result.index.set_levels(new_level, level=-1)
2217 else:
2218 result.index = result.index.to_period(self.freq)
2219 return result
2222@set_module("pandas.api.typing")
2223# error: Definition of "ax" in base class "_GroupByMixin" is incompatible
2224# with definition in base class "DatetimeIndexResampler"
2225class DatetimeIndexResamplerGroupBy( # type: ignore[misc]
2226 _GroupByMixin, DatetimeIndexResampler
2227):
2228 """
2229 Provides a resample of a groupby implementation
2230 """
2232 @property
2233 def _resampler_cls(self):
2234 return DatetimeIndexResampler
2237class PeriodIndexResampler(DatetimeIndexResampler):
2238 # error: Incompatible types in assignment (expression has type "PeriodIndex", base
2239 # class "DatetimeIndexResampler" defined the type as "DatetimeIndex")
2240 ax: PeriodIndex # type: ignore[assignment]
2242 @property
2243 def _resampler_for_grouping(self):
2244 return PeriodIndexResamplerGroupBy
2246 def _get_binner_for_time(self):
2247 return self._timegrouper._get_period_bins(self.ax)
2249 def _convert_obj(self, obj: NDFrameT) -> NDFrameT:
2250 obj = super()._convert_obj(obj)
2252 if self._from_selection:
2253 # see GH 14008, GH 12871
2254 msg = (
2255 "Resampling from level= or on= selection "
2256 "with a PeriodIndex is not currently supported, "
2257 "use .set_index(...) to explicitly set index"
2258 )
2259 raise NotImplementedError(msg)
2261 return obj
2263 def _downsample(self, how, **kwargs):
2264 """
2265 Downsample the cython defined function.
2267 Parameters
2268 ----------
2269 how : string / cython mapped function
2270 **kwargs : kw args passed to how function
2271 """
2272 ax = self.ax
2274 if not (
2275 is_subperiod(ax.freq, self.freq)
2276 or is_superperiod(ax.freq, self.freq)
2277 or ax.freq == self.freq
2278 ):
2279 raise IncompatibleFrequency(
2280 f"Frequency {ax.freq} cannot be resampled to {self.freq}, "
2281 "as they are not sub or super periods"
2282 )
2284 if not len(ax):
2285 # reset to the new freq
2286 obj = self._obj_with_exclusions.copy()
2287 obj.index = obj.index.asfreq(self.freq)
2288 return obj
2290 return self._groupby_and_aggregate(how, **kwargs)
2292 def _upsample(self, method, limit: int | None = None, fill_value=None):
2293 """
2294 Parameters
2295 ----------
2296 method : {'backfill', 'bfill', 'pad', 'ffill'}
2297 Method for upsampling.
2298 limit : int, default None
2299 Maximum size gap to fill when reindexing.
2300 fill_value : scalar, default None
2301 Value to use for missing values.
2302 """
2303 ax = self.ax
2304 obj = self.obj
2305 new_index = self.binner
2307 # Start vs. end of period
2308 memb = ax.asfreq(self.freq, how=self.convention)
2310 # Get the fill indexer
2311 if method == "asfreq":
2312 method = None
2313 indexer = memb.get_indexer(new_index, method=method, limit=limit)
2314 new_obj = _take_new_index(
2315 obj,
2316 indexer,
2317 new_index,
2318 )
2319 return self._wrap_result(new_obj)
2322@set_module("pandas.api.typing")
2323# error: Definition of "ax" in base class "_GroupByMixin" is incompatible with
2324# definition in base class "PeriodIndexResampler"
2325class PeriodIndexResamplerGroupBy( # type: ignore[misc]
2326 _GroupByMixin, PeriodIndexResampler
2327):
2328 """
2329 Provides a resample of a groupby implementation.
2330 """
2332 @property
2333 def _resampler_cls(self):
2334 return PeriodIndexResampler
2337class TimedeltaIndexResampler(DatetimeIndexResampler):
2338 # error: Incompatible types in assignment (expression has type "TimedeltaIndex",
2339 # base class "DatetimeIndexResampler" defined the type as "DatetimeIndex")
2340 ax: TimedeltaIndex # type: ignore[assignment]
2342 @property
2343 def _resampler_for_grouping(self):
2344 return TimedeltaIndexResamplerGroupBy
2346 def _get_binner_for_time(self):
2347 return self._timegrouper._get_time_delta_bins(self.ax)
2349 def _adjust_binner_for_upsample(self, binner):
2350 """
2351 Adjust our binner when upsampling.
2353 The range of a new index is allowed to be greater than original range
2354 so we don't need to change the length of a binner, GH 13022
2355 """
2356 return binner
2359@set_module("pandas.api.typing")
2360# error: Definition of "ax" in base class "_GroupByMixin" is incompatible with
2361# definition in base class "DatetimeIndexResampler"
2362class TimedeltaIndexResamplerGroupBy( # type: ignore[misc]
2363 _GroupByMixin, TimedeltaIndexResampler
2364):
2365 """
2366 Provides a resample of a groupby implementation.
2367 """
2369 @property
2370 def _resampler_cls(self):
2371 return TimedeltaIndexResampler
2374def get_resampler(obj: Series | DataFrame, **kwds) -> Resampler:
2375 """
2376 Create a TimeGrouper and return our resampler.
2377 """
2378 tg = TimeGrouper(obj, **kwds) # type: ignore[arg-type]
2379 return tg._get_resampler(obj)
2382get_resampler.__doc__ = Resampler.__doc__
2385def get_resampler_for_grouping(
2386 groupby: GroupBy,
2387 rule,
2388 how=None,
2389 fill_method=None,
2390 limit: int | None = None,
2391 on=None,
2392 **kwargs,
2393) -> Resampler:
2394 """
2395 Return our appropriate resampler when grouping as well.
2396 """
2397 # .resample uses 'on' similar to how .groupby uses 'key'
2398 tg = TimeGrouper(freq=rule, key=on, **kwargs)
2399 resampler = tg._get_resampler(groupby.obj)
2400 return resampler._get_resampler_for_grouping(groupby=groupby, key=tg.key)
2403@set_module("pandas.api.typing")
2404class TimeGrouper(Grouper):
2405 """
2406 Custom groupby class for time-interval grouping.
2408 Parameters
2409 ----------
2410 freq : pandas date offset or offset alias for identifying bin edges
2411 closed : closed end of interval; 'left' or 'right'
2412 label : interval boundary to use for labeling; 'left' or 'right'
2413 convention : {'start', 'end', 'e', 's'}
2414 If axis is PeriodIndex
2415 """
2417 _attributes = (
2418 *Grouper._attributes,
2419 "closed",
2420 "label",
2421 "how",
2422 "convention",
2423 "origin",
2424 "offset",
2425 )
2427 origin: TimeGrouperOrigin
2429 def __init__(
2430 self,
2431 obj: Grouper | None = None,
2432 freq: Frequency = "Min",
2433 key: str | None = None,
2434 closed: Literal["left", "right"] | None = None,
2435 label: Literal["left", "right"] | None = None,
2436 how: str = "mean",
2437 fill_method=None,
2438 limit: int | None = None,
2439 convention: Literal["start", "end", "e", "s"] | None = None,
2440 origin: (
2441 Literal["epoch", "start", "start_day", "end", "end_day"]
2442 | TimestampConvertibleTypes
2443 ) = "start_day",
2444 offset: TimedeltaConvertibleTypes | None = None,
2445 group_keys: bool = False,
2446 **kwargs,
2447 ) -> None:
2448 # Check for correctness of the keyword arguments which would
2449 # otherwise silently use the default if misspelled
2450 if label not in {None, "left", "right"}:
2451 raise ValueError(f"Unsupported value {label} for `label`")
2452 if closed not in {None, "left", "right"}:
2453 raise ValueError(f"Unsupported value {closed} for `closed`")
2454 if convention not in {None, "start", "end", "e", "s"}:
2455 raise ValueError(f"Unsupported value {convention} for `convention`")
2457 if (key is None and obj is not None and isinstance(obj.index, PeriodIndex)) or ( # type: ignore[attr-defined]
2458 key is not None
2459 and obj is not None
2460 and getattr(obj[key], "dtype", None) == "period" # type: ignore[index]
2461 ):
2462 freq = to_offset(freq, is_period=True)
2463 else:
2464 freq = to_offset(freq)
2466 # GH#44996: the Day check is deferred to _get_resampler, where the
2467 # binning axis is known.
2468 if not isinstance(freq, (Tick, Day)):
2469 if offset is not None:
2470 warnings.warn(
2471 "The 'offset' keyword does not take effect when resampling "
2472 "with a 'freq' that is not Tick-like (h, m, s, ms, us, ns)",
2473 RuntimeWarning,
2474 stacklevel=find_stack_level(),
2475 )
2476 if origin != "start_day":
2477 warnings.warn(
2478 "The 'origin' keyword does not take effect when resampling "
2479 "with a 'freq' that is not Tick-like (h, m, s, ms, us, ns)",
2480 RuntimeWarning,
2481 stacklevel=find_stack_level(),
2482 )
2484 end_types = {"ME", "YE", "QE", "BME", "BYE", "BQE", "W"}
2485 rule = freq.rule_code
2486 if rule in end_types or ("-" in rule and rule[: rule.find("-")] in end_types):
2487 if closed is None:
2488 closed = "right"
2489 if label is None:
2490 label = "right"
2491 # The backward resample sets ``closed`` to ``'right'`` by default
2492 # since the last value should be considered as the edge point for
2493 # the last bin. When origin in "end" or "end_day", the value for a
2494 # specific ``Timestamp`` index stands for the resample result from
2495 # the current ``Timestamp`` minus ``freq`` to the current
2496 # ``Timestamp`` with a right close.
2497 elif origin in ["end", "end_day"]:
2498 if closed is None:
2499 closed = "right"
2500 if label is None:
2501 label = "right"
2502 else:
2503 if closed is None:
2504 closed = "left"
2505 if label is None:
2506 label = "left"
2508 self.closed = closed
2509 self.label = label
2510 self.convention = convention if convention is not None else "e"
2511 self.how = how
2512 self.fill_method = fill_method
2513 self.limit = limit
2514 self.group_keys = group_keys
2515 self._arrow_dtype: ArrowDtype | None = None
2517 if origin in ("epoch", "start", "start_day", "end", "end_day"):
2518 # error: Incompatible types in assignment (expression has type "Union[Union[
2519 # Timestamp, datetime, datetime64, signedinteger[_64Bit], float, str],
2520 # Literal['epoch', 'start', 'start_day', 'end', 'end_day']]", variable has
2521 # type "Union[Timestamp, Literal['epoch', 'start', 'start_day', 'end',
2522 # 'end_day']]")
2523 self.origin = origin # type: ignore[assignment]
2524 else:
2525 try:
2526 self.origin = Timestamp(origin)
2527 except (ValueError, TypeError) as err:
2528 raise ValueError(
2529 "'origin' should be equal to 'epoch', 'start', 'start_day', "
2530 "'end', 'end_day' or "
2531 f"should be a Timestamp convertible type. Got '{origin}' instead."
2532 ) from err
2534 try:
2535 self.offset = Timedelta(offset) if offset is not None else None
2536 except (ValueError, TypeError) as err:
2537 raise ValueError(
2538 "'offset' should be a Timedelta convertible type. "
2539 f"Got '{offset}' instead."
2540 ) from err
2542 # always sort time groupers
2543 kwargs["sort"] = True
2545 super().__init__(freq=freq, key=key, **kwargs)
2547 def _get_resampler(self, obj: NDFrame) -> Resampler:
2548 """
2549 Return my resampler or raise if we have an invalid axis.
2551 Parameters
2552 ----------
2553 obj : Series or DataFrame
2555 Returns
2556 -------
2557 Resampler
2559 Raises
2560 ------
2561 TypeError if incompatible axis
2563 """
2564 _, ax, _ = self._set_grouper(obj, gpr_index=None)
2566 # GH#44996: with a Day freq, origin/offset take effect only on a
2567 # tz-naive DatetimeIndex (where Day is equivalent to 24h), except
2568 # that offset also takes effect in _get_time_delta_bins.
2569 if isinstance(self.freq, Day) and not (
2570 isinstance(ax, DatetimeIndex) and ax.tz is None
2571 ):
2572 if self.offset is not None and not isinstance(ax, TimedeltaIndex):
2573 warnings.warn(
2574 "The 'offset' keyword does not take effect when resampling "
2575 "with a Day 'freq' unless the index is a timezone-naive "
2576 "DatetimeIndex",
2577 RuntimeWarning,
2578 stacklevel=find_stack_level(),
2579 )
2580 if self.origin != "start_day":
2581 warnings.warn(
2582 "The 'origin' keyword does not take effect when resampling "
2583 "with a Day 'freq' unless the index is a timezone-naive "
2584 "DatetimeIndex",
2585 RuntimeWarning,
2586 stacklevel=find_stack_level(),
2587 )
2589 if isinstance(ax, DatetimeIndex):
2590 return DatetimeIndexResampler(
2591 obj,
2592 timegrouper=self,
2593 group_keys=self.group_keys,
2594 gpr_index=ax,
2595 )
2596 elif isinstance(ax, PeriodIndex):
2597 return PeriodIndexResampler(
2598 obj,
2599 timegrouper=self,
2600 group_keys=self.group_keys,
2601 gpr_index=ax,
2602 )
2603 elif isinstance(ax, TimedeltaIndex):
2604 return TimedeltaIndexResampler(
2605 obj,
2606 timegrouper=self,
2607 group_keys=self.group_keys,
2608 gpr_index=ax,
2609 )
2611 raise TypeError(
2612 "Only valid with DatetimeIndex, "
2613 "TimedeltaIndex or PeriodIndex, "
2614 f"but got an instance of '{type(ax).__name__}'"
2615 )
2617 def _get_grouper(
2618 self, obj: NDFrameT, validate: bool = True, observed: bool = True
2619 ) -> tuple[BinGrouper, NDFrameT]:
2620 """
2621 Parameters
2622 ----------
2623 obj : Series or DataFrame
2624 Object being grouped.
2625 validate : bool, default True
2626 Unused. Only for compatibility with ``Grouper._get_grouper``.
2627 observed : bool, default True
2628 Unused. Only for compatibility with ``Grouper._get_grouper``.
2630 Returns
2631 -------
2632 A tuple of grouper, obj (possibly sorted)
2633 """
2634 # create the resampler and return our binner
2635 r = self._get_resampler(obj)
2636 return r._grouper, cast("NDFrameT", r.obj)
2638 def _get_time_bins(self, ax: DatetimeIndex):
2639 if not isinstance(ax, DatetimeIndex):
2640 raise TypeError(
2641 "axis must be a DatetimeIndex, but got "
2642 f"an instance of {type(ax).__name__}"
2643 )
2645 if len(ax) == 0:
2646 binner = labels = DatetimeIndex(
2647 data=[], freq=self.freq, name=ax.name, dtype=ax.dtype
2648 )
2649 return binner, [], labels
2651 # GH#43486: filter NaTs up front, mirroring _get_period_bins
2652 nat_count = 0
2653 if ax.hasnans:
2654 nat_count = ax.isna().sum() # type: ignore[assignment]
2655 ax = ax[~ax.isna()]
2657 if len(ax) == 0:
2658 # all-NaT case
2659 binner = labels = DatetimeIndex(
2660 data=[], freq=self.freq, name=ax.name, dtype=ax.dtype
2661 )
2662 bins = np.array([], dtype=np.int64)
2663 binner = binner.insert(0, NaT)
2664 labels = labels.insert(0, NaT)
2665 return binner, np.insert(bins, 0, nat_count), labels
2667 first, last = _get_timestamp_range_edges(
2668 ax.min(), # type: ignore[arg-type]
2669 ax.max(), # type: ignore[arg-type]
2670 self.freq,
2671 unit=ax.unit,
2672 closed=self.closed,
2673 origin=self.origin,
2674 offset=self.offset,
2675 )
2676 # GH #12037
2677 # use first/last directly instead of call replace() on them
2678 # because replace() will swallow the nanosecond part
2679 # thus last bin maybe slightly before the end if the end contains
2680 # nanosecond part and lead to `Values falls after last bin` error
2681 # GH 25758: If DST lands at midnight (e.g. 'America/Havana'), user feedback
2682 # has noted that ambiguous=True provides the most sensible result
2683 binner = labels = date_range(
2684 freq=self.freq,
2685 start=first,
2686 end=last,
2687 tz=ax.tz,
2688 name=ax.name,
2689 ambiguous=True,
2690 nonexistent="shift_forward",
2691 unit=ax.unit,
2692 )
2694 ax_values = ax.asi8
2695 binner, bin_edges = self._adjust_bin_edges(binner, ax_values)
2697 # general version, knowing nothing about relative frequencies
2698 bins = lib.generate_bins_dt64(ax_values, bin_edges, self.closed, hasnans=False)
2700 if self.closed == "right":
2701 labels = binner
2702 if self.label == "right":
2703 labels = labels[1:]
2704 elif self.label == "right":
2705 labels = labels[1:]
2707 if nat_count > 0:
2708 # shift bins by the number of NaT (they sort to the front of asi8)
2709 bins = bins + nat_count
2710 bins = np.insert(bins, 0, nat_count)
2711 binner = binner.insert(0, NaT)
2712 labels = labels.insert(0, NaT)
2714 # if we end up with more labels than bins
2715 # adjust the labels
2716 # GH4076
2717 if len(bins) < len(labels):
2718 labels = labels[: len(bins)]
2720 return binner, bins, labels
2722 def _adjust_bin_edges(
2723 self, binner: DatetimeIndex, ax_values: npt.NDArray[np.int64]
2724 ) -> tuple[DatetimeIndex, npt.NDArray[np.int64]]:
2725 # Some hacks for > daily data, see #1471, #1458, #1483
2727 if self.freq.rule_code in ("BME", "ME", "W") or self.freq.rule_code.split("-")[
2728 0
2729 ] in (
2730 "BQE",
2731 "BYE",
2732 "QE",
2733 "YE",
2734 "W",
2735 ):
2736 # If the right end-point is on the last day of the month, roll forwards
2737 # until the last moment of that day. Note that we only do this for offsets
2738 # which correspond to the end of a super-daily period - "month start", for
2739 # example, is excluded.
2740 if self.closed == "right":
2741 # GH 21459, GH 9119: Adjust the bins relative to the wall time
2742 edges_dti = binner.tz_localize(None)
2743 edges_dti = (
2744 edges_dti
2745 + Timedelta(days=1).as_unit(edges_dti.unit)
2746 - Timedelta(1, unit=edges_dti.unit).as_unit(edges_dti.unit)
2747 )
2748 bin_edges = edges_dti.tz_localize(binner.tz).asi8
2749 else:
2750 bin_edges = binner.asi8
2752 # intraday values on last day
2753 if bin_edges[-2] > ax_values.max():
2754 bin_edges = bin_edges[:-1]
2755 binner = binner[:-1]
2756 else:
2757 bin_edges = binner.asi8
2758 return binner, bin_edges
2760 def _get_time_delta_bins(self, ax: TimedeltaIndex):
2761 if not isinstance(ax, TimedeltaIndex):
2762 raise TypeError(
2763 "axis must be a TimedeltaIndex, but got "
2764 f"an instance of {type(ax).__name__}"
2765 )
2767 if not isinstance(self.freq, (Tick, Day)):
2768 # GH#51896
2769 raise ValueError(
2770 "Resampling on a TimedeltaIndex requires fixed-duration `freq`, "
2771 f"e.g. '24h' or '3D', not {self.freq}"
2772 )
2774 if not len(ax):
2775 binner = labels = TimedeltaIndex(data=[], freq=self.freq, name=ax.name)
2776 return binner, [], labels
2778 start, end = ax.min(), ax.max()
2780 if self.closed == "right":
2781 end += self.freq # type: ignore[operator]
2783 labels = binner = timedelta_range(
2784 start=start, end=end, freq=self.freq, name=ax.name
2785 )
2787 end_stamps = labels
2788 if self.closed == "left":
2789 end_stamps += self.freq
2791 bins = ax.searchsorted(end_stamps, side=self.closed)
2793 if self.offset:
2794 # GH 10530 & 31809
2795 labels += self.offset
2797 return binner, bins, labels
2799 def _get_time_period_bins(self, ax: DatetimeIndex):
2800 if not isinstance(ax, DatetimeIndex):
2801 raise TypeError(
2802 "axis must be a DatetimeIndex, but got "
2803 f"an instance of {type(ax).__name__}"
2804 )
2806 freq = self.freq
2808 if len(ax) == 0:
2809 binner = labels = PeriodIndex(
2810 data=[], freq=freq, name=ax.name, dtype=ax.dtype
2811 )
2812 return binner, [], labels
2814 labels = binner = period_range(start=ax[0], end=ax[-1], freq=freq, name=ax.name)
2816 end_stamps = (labels + freq).asfreq(freq, "s").to_timestamp()
2817 if ax.tz:
2818 end_stamps = end_stamps.tz_localize(ax.tz)
2819 bins = ax.searchsorted(end_stamps, side="left")
2821 return binner, bins, labels
2823 def _get_period_bins(self, ax: PeriodIndex):
2824 if not isinstance(ax, PeriodIndex):
2825 raise TypeError(
2826 "axis must be a PeriodIndex, but got "
2827 f"an instance of {type(ax).__name__}"
2828 )
2830 memb = ax.asfreq(self.freq, how=self.convention)
2832 # NaT handling as in pandas._lib.lib.generate_bins_dt64()
2833 nat_count = 0
2834 if memb.hasnans:
2835 # error: Incompatible types in assignment (expression has type
2836 # "bool_", variable has type "int") [assignment]
2837 nat_count = np.sum(memb._isnan) # type: ignore[assignment]
2838 memb = memb[~memb._isnan]
2840 if not len(memb):
2841 # index contains no valid (non-NaT) values
2842 bins = np.array([], dtype=np.int64)
2843 binner = labels = PeriodIndex(data=[], freq=self.freq, name=ax.name)
2844 if len(ax) > 0:
2845 # index is all NaT
2846 binner, bins, labels = _insert_nat_bin(binner, bins, labels, len(ax))
2847 return binner, bins, labels
2849 freq_mult = self.freq.n
2851 start = ax.min().asfreq(self.freq, how=self.convention) # type: ignore[attr-defined]
2852 end = ax.max().asfreq(self.freq, how="end") # type: ignore[attr-defined]
2853 bin_shift = 0
2855 if isinstance(self.freq, Tick):
2856 # GH 23882 & 31809: get adjusted bin edge labels with 'origin'
2857 # and 'origin' support. This call only makes sense if the freq is a
2858 # Tick since offset and origin are only used in those cases.
2859 # Not doing this check could create an extra empty bin.
2860 p_start, end = _get_period_range_edges(
2861 start,
2862 end,
2863 self.freq,
2864 closed=self.closed,
2865 origin=self.origin,
2866 offset=self.offset,
2867 )
2869 # Get offset for bin edge (not label edge) adjustment
2870 start_offset = Period(start, self.freq) - Period(p_start, self.freq)
2871 # error: Item "Period" of "Union[Period, Any]" has no attribute "n"
2872 bin_shift = start_offset.n % freq_mult # type: ignore[union-attr]
2873 start = p_start
2875 labels = binner = period_range(
2876 start=start, end=end, freq=self.freq, name=ax.name
2877 )
2879 i8 = memb.asi8
2881 # when upsampling to subperiods, we need to generate enough bins
2882 expected_bins_count = len(binner) * freq_mult
2883 i8_extend = expected_bins_count - (i8[-1] - i8[0])
2884 rng = np.arange(i8[0], i8[-1] + i8_extend, freq_mult)
2885 rng += freq_mult
2886 # adjust bin edge indexes to account for base
2887 rng -= bin_shift
2889 # Wrap in PeriodArray for PeriodArray.searchsorted
2890 prng = type(memb._data)(rng, dtype=memb.dtype)
2891 bins = memb.searchsorted(prng, side="left")
2893 if nat_count > 0:
2894 binner, bins, labels = _insert_nat_bin(binner, bins, labels, nat_count)
2896 return binner, bins, labels
2898 def _set_grouper(
2899 self, obj: NDFrameT, sort: bool = False, *, gpr_index: Index | None = None
2900 ) -> tuple[NDFrameT, Index, npt.NDArray[np.intp] | None]:
2901 obj, ax, indexer = super()._set_grouper(obj, sort, gpr_index=gpr_index)
2902 if isinstance(ax.dtype, ArrowDtype) and ax.dtype.kind in "Mm":
2903 self._arrow_dtype = ax.dtype
2904 ax = Index(
2905 cast("ArrowExtensionArray", ax.array)._maybe_convert_datelike_array(),
2906 name=ax.name,
2907 )
2908 return obj, ax, indexer
2911@overload
2912def _take_new_index(
2913 obj: DataFrame, indexer: npt.NDArray[np.intp], new_index: Index
2914) -> DataFrame: ...
2917@overload
2918def _take_new_index(
2919 obj: Series, indexer: npt.NDArray[np.intp], new_index: Index
2920) -> Series: ...
2923def _take_new_index(
2924 obj: DataFrame | Series,
2925 indexer: npt.NDArray[np.intp],
2926 new_index: Index,
2927) -> DataFrame | Series:
2928 if isinstance(obj, ABCSeries):
2929 new_values = algos.take_nd(obj._values, indexer)
2930 return obj._constructor(new_values, index=new_index, name=obj.name)
2931 elif isinstance(obj, ABCDataFrame):
2932 new_mgr = obj._mgr.reindex_indexer(new_axis=new_index, indexer=indexer, axis=1)
2933 return obj._constructor_from_mgr(new_mgr, axes=new_mgr.axes)
2934 else:
2935 raise ValueError("'obj' should be either a Series or a DataFrame")
2938def _get_timestamp_range_edges(
2939 first: Timestamp,
2940 last: Timestamp,
2941 freq: BaseOffset,
2942 unit: TimeUnit,
2943 closed: Literal["right", "left"] = "left",
2944 origin: TimeGrouperOrigin = "start_day",
2945 offset: Timedelta | None = None,
2946) -> tuple[Timestamp, Timestamp]:
2947 """
2948 Adjust the `first` Timestamp to the preceding Timestamp that resides on
2949 the provided offset. Adjust the `last` Timestamp to the following
2950 Timestamp that resides on the provided offset. Input Timestamps that
2951 already reside on the offset will be adjusted depending on the type of
2952 offset and the `closed` parameter.
2954 Parameters
2955 ----------
2956 first : pd.Timestamp
2957 The beginning Timestamp of the range to be adjusted.
2958 last : pd.Timestamp
2959 The ending Timestamp of the range to be adjusted.
2960 freq : pd.DateOffset
2961 The dateoffset to which the Timestamps will be adjusted.
2962 closed : {'right', 'left'}, default "left"
2963 Which side of bin interval is closed.
2964 origin : {'epoch', 'start', 'start_day'} or Timestamp, default 'start_day'
2965 The timestamp on which to adjust the grouping. The timezone of origin must
2966 match the timezone of the index.
2967 If a timestamp is not used, these values are also supported:
2969 - 'epoch': `origin` is 1970-01-01
2970 - 'start': `origin` is the first value of the timeseries
2971 - 'start_day': `origin` is the first day at midnight of the timeseries
2972 offset : pd.Timedelta, default is None
2973 An offset timedelta added to the origin.
2975 Returns
2976 -------
2977 A tuple of length 2, containing the adjusted pd.Timestamp objects.
2978 """
2979 if isinstance(freq, Tick) or (isinstance(freq, Day) and first.tz is None):
2980 index_tz = first.tz
2981 if isinstance(origin, Timestamp) and (origin.tz is None) != (index_tz is None):
2982 raise ValueError("The origin must have the same timezone as the index.")
2983 if origin == "epoch":
2984 # set the epoch based on the timezone to have similar bins results when
2985 # resampling on the same kind of indexes on different timezones
2986 origin = Timestamp("1970-01-01", tz=index_tz)
2988 if isinstance(freq, Day):
2989 # GH#44996: tz-naive Day should produce the same bins as the
2990 # equivalent Hour offset.
2991 freq = Hour(24 * freq.n)
2993 first, last = _adjust_dates_anchored(
2994 first,
2995 last,
2996 freq,
2997 closed=closed,
2998 origin=origin,
2999 offset=offset,
3000 unit=unit,
3001 )
3002 else:
3003 first = first.normalize()
3004 last = last.normalize()
3006 if closed == "left":
3007 first = Timestamp(freq.rollback(first))
3008 else:
3009 first = Timestamp(first - freq)
3011 last = Timestamp(last + freq)
3013 return first, last
3016def _get_period_range_edges(
3017 first: Period,
3018 last: Period,
3019 freq: BaseOffset,
3020 closed: Literal["right", "left"] = "left",
3021 origin: TimeGrouperOrigin = "start_day",
3022 offset: Timedelta | None = None,
3023) -> tuple[Period, Period]:
3024 """
3025 Adjust the provided `first` and `last` Periods to the respective Period of
3026 the given offset that encompasses them.
3028 Parameters
3029 ----------
3030 first : pd.Period
3031 The beginning Period of the range to be adjusted.
3032 last : pd.Period
3033 The ending Period of the range to be adjusted.
3034 freq : pd.DateOffset
3035 The freq to which the Periods will be adjusted.
3036 closed : {'right', 'left'}, default "left"
3037 Which side of bin interval is closed.
3038 origin : {'epoch', 'start', 'start_day'}, Timestamp, default 'start_day'
3039 The timestamp on which to adjust the grouping. The timezone of origin must
3040 match the timezone of the index.
3042 If a timestamp is not used, these values are also supported:
3044 - 'epoch': `origin` is 1970-01-01
3045 - 'start': `origin` is the first value of the timeseries
3046 - 'start_day': `origin` is the first day at midnight of the timeseries
3047 offset : pd.Timedelta, default is None
3048 An offset timedelta added to the origin.
3050 Returns
3051 -------
3052 A tuple of length 2, containing the adjusted pd.Period objects.
3053 """
3054 if not all(isinstance(obj, Period) for obj in [first, last]):
3055 raise TypeError("'first' and 'last' must be instances of type Period")
3057 # GH 23882
3058 first_ts = first.to_timestamp()
3059 last_ts = last.to_timestamp()
3060 adjust_first = not freq.is_on_offset(first_ts)
3061 adjust_last = freq.is_on_offset(last_ts)
3063 first_ts, last_ts = _get_timestamp_range_edges(
3064 first_ts, last_ts, freq, unit="ns", closed=closed, origin=origin, offset=offset
3065 )
3067 first = (first_ts + int(adjust_first) * freq).to_period(freq)
3068 last = (last_ts - int(adjust_last) * freq).to_period(freq)
3069 return first, last
3072def _insert_nat_bin(
3073 binner: PeriodIndex, bins: np.ndarray, labels: PeriodIndex, nat_count: int
3074) -> tuple[PeriodIndex, np.ndarray, PeriodIndex]:
3075 # NaT handling as in pandas._lib.lib.generate_bins_dt64()
3076 # shift bins by the number of NaT
3077 assert nat_count > 0
3078 bins += nat_count
3079 bins = np.insert(bins, 0, nat_count)
3081 # Incompatible types in assignment (expression has type "Index", variable
3082 # has type "PeriodIndex")
3083 binner = binner.insert(0, NaT) # type: ignore[assignment]
3084 # Incompatible types in assignment (expression has type "Index", variable
3085 # has type "PeriodIndex")
3086 labels = labels.insert(0, NaT) # type: ignore[assignment]
3087 return binner, bins, labels
3090def _adjust_dates_anchored(
3091 first: Timestamp,
3092 last: Timestamp,
3093 freq: Tick,
3094 closed: Literal["right", "left"] = "right",
3095 origin: TimeGrouperOrigin = "start_day",
3096 offset: Timedelta | None = None,
3097 unit: TimeUnit = "ns",
3098) -> tuple[Timestamp, Timestamp]:
3099 # First and last offsets should be calculated from the start day to fix an
3100 # error cause by resampling across multiple days when a one day period is
3101 # not a multiple of the frequency. See GH 8683
3102 # To handle frequencies that are not multiple or divisible by a day we let
3103 # the possibility to define a fixed origin timestamp. See GH 31809
3104 first = first.as_unit(unit)
3105 last = last.as_unit(unit)
3106 if offset is not None:
3107 offset = offset.as_unit(unit)
3109 freq_value = Timedelta(freq).as_unit(unit)._value
3111 origin_timestamp = 0 # origin == "epoch"
3112 if origin == "start_day":
3113 origin_timestamp = first.normalize()._value
3114 elif origin == "start":
3115 origin_timestamp = first._value
3116 elif isinstance(origin, Timestamp):
3117 origin_timestamp = origin.as_unit(unit)._value
3118 elif origin in ["end", "end_day"]:
3119 origin_last = last if origin == "end" else last.ceil("D")
3120 sub_freq_times = (origin_last._value - first._value) // freq_value
3121 if closed == "left":
3122 sub_freq_times += 1
3123 # error: Incompatible types in assignment (expression has type
3124 # "Timestamp | NaTType", variable has type "Timestamp")
3125 first = origin_last - sub_freq_times * freq # type: ignore[assignment]
3126 origin_timestamp = first._value
3127 origin_timestamp += offset._value if offset else 0
3129 # GH 10117 & GH 19375. If first and last contain timezone information,
3130 # Perform the calculation in UTC in order to avoid localizing on an
3131 # Ambiguous or Nonexistent time.
3132 first_tzinfo = first.tzinfo
3133 last_tzinfo = last.tzinfo
3134 if first_tzinfo is not None:
3135 first = first.tz_convert("UTC")
3136 if last_tzinfo is not None:
3137 last = last.tz_convert("UTC")
3139 foffset = (first._value - origin_timestamp) % freq_value
3140 loffset = (last._value - origin_timestamp) % freq_value
3142 if closed == "right":
3143 if foffset > 0:
3144 # roll back
3145 fresult_int = first._value - foffset
3146 else:
3147 fresult_int = first._value - freq_value
3149 if loffset > 0:
3150 # roll forward
3151 lresult_int = last._value + (freq_value - loffset)
3152 else:
3153 # already the end of the road
3154 lresult_int = last._value
3155 else: # closed == 'left'
3156 if foffset > 0:
3157 fresult_int = first._value - foffset
3158 else:
3159 # start of the road
3160 fresult_int = first._value
3162 if loffset > 0:
3163 # roll forward
3164 lresult_int = last._value + (freq_value - loffset)
3165 else:
3166 lresult_int = last._value + freq_value
3167 fresult = Timestamp._from_value_and_reso(fresult_int, first._creso, first_tzinfo)
3168 lresult = Timestamp._from_value_and_reso(lresult_int, last._creso, last_tzinfo)
3169 return fresult, lresult
3172def asfreq(
3173 obj: NDFrameT,
3174 freq,
3175 method=None,
3176 how=None,
3177 normalize: bool = False,
3178 fill_value=None,
3179) -> NDFrameT:
3180 """
3181 Utility frequency conversion method for Series/DataFrame.
3183 See :meth:`pandas.NDFrame.asfreq` for full documentation.
3184 """
3185 if isinstance(obj.index, PeriodIndex):
3186 if method is not None:
3187 raise NotImplementedError("'method' argument is not supported")
3189 if how is None:
3190 how = "E"
3192 if isinstance(freq, BaseOffset):
3193 if hasattr(freq, "_period_dtype_code"):
3194 freq = PeriodDtype(freq)._freqstr
3196 new_obj = obj.copy()
3197 new_obj.index = obj.index.asfreq(freq, how=how)
3199 elif len(obj.index) == 0:
3200 new_obj = obj.copy()
3202 new_obj.index = _asfreq_compat(obj.index, freq)
3203 else:
3204 unit: TimeUnit = "ns"
3205 if isinstance(obj.index, DatetimeIndex):
3206 # TODO: should we disallow non-DatetimeIndex?
3207 unit = obj.index.unit
3208 dti = date_range(obj.index.min(), obj.index.max(), freq=freq, unit=unit)
3209 dti.name = obj.index.name
3210 new_obj = obj.reindex(dti, method=method, fill_value=fill_value)
3211 if normalize:
3212 new_obj.index = new_obj.index.normalize()
3214 return new_obj
3217def _asfreq_compat(index: FreqIndexT, freq) -> FreqIndexT:
3218 """
3219 Helper to mimic asfreq on (empty) DatetimeIndex and TimedeltaIndex.
3221 Parameters
3222 ----------
3223 index : PeriodIndex, DatetimeIndex, or TimedeltaIndex
3224 freq : DateOffset
3226 Returns
3227 -------
3228 same type as index
3229 """
3230 if len(index) != 0:
3231 # This should never be reached, always checked by the caller
3232 raise ValueError(
3233 "Can only set arbitrary freq for empty DatetimeIndex or TimedeltaIndex"
3234 )
3235 if isinstance(index, PeriodIndex):
3236 new_index = index.asfreq(freq=freq)
3237 elif isinstance(index, DatetimeIndex):
3238 new_index = DatetimeIndex([], dtype=index.dtype, freq=freq, name=index.name)
3239 elif isinstance(index, TimedeltaIndex):
3240 new_index = TimedeltaIndex([], dtype=index.dtype, freq=freq, name=index.name)
3241 else: # pragma: no cover
3242 raise TypeError(type(index))
3243 return new_index