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

731 statements  

1from __future__ import annotations 

2 

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 

15 

16import numpy as np 

17 

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 

34 

35from pandas.core.dtypes.dtypes import ( 

36 ArrowDtype, 

37 PeriodDtype, 

38) 

39from pandas.core.dtypes.generic import ( 

40 ABCDataFrame, 

41 ABCSeries, 

42) 

43 

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 

75 

76from pandas.tseries.frequencies import ( 

77 is_subperiod, 

78 is_superperiod, 

79) 

80from pandas.tseries.offsets import ( 

81 Day, 

82 Hour, 

83 Tick, 

84) 

85 

86if TYPE_CHECKING: 

87 from collections.abc import ( 

88 Callable, 

89 Hashable, 

90 ) 

91 

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 ) 

109 

110 from pandas import ( 

111 DataFrame, 

112 Series, 

113 ) 

114 from pandas.core.arrays import ArrowExtensionArray 

115 from pandas.core.generic import NDFrame 

116 

117 

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. 

123 

124 It's easiest to use obj.resample(...) to use Resampler. 

125 

126 Parameters 

127 ---------- 

128 obj : Series or DataFrame 

129 groupby : TimeGrouper 

130 

131 Returns 

132 ------- 

133 a Resampler of the appropriate type 

134 

135 Notes 

136 ----- 

137 After resampling, see aggregate, apply, and transform functions. 

138 """ 

139 

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"}) 

145 

146 # to the groupby descriptor 

147 _attributes = [ 

148 "freq", 

149 "closed", 

150 "label", 

151 "convention", 

152 "origin", 

153 "offset", 

154 ] 

155 

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 

173 

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() 

183 

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)}]" 

195 

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] 

204 

205 return object.__getattribute__(self, attr) 

206 

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 ) 

218 

219 def _convert_obj(self, obj: NDFrameT) -> NDFrameT: 

220 """ 

221 Provide any conversions for the object in order to correctly handle. 

222 

223 Parameters 

224 ---------- 

225 obj : Series or DataFrame 

226 

227 Returns 

228 ------- 

229 Series or DataFrame 

230 """ 

231 return obj._consolidate() 

232 

233 def _get_binner_for_time(self): 

234 raise AbstractMethodError(self) 

235 

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 

249 

250 @overload 

251 def pipe( 

252 self, 

253 func: Callable[Concatenate[Self, P], T], 

254 *args: P.args, 

255 **kwargs: P.kwargs, 

256 ) -> T: ... 

257 

258 @overload 

259 def pipe( 

260 self, 

261 func: tuple[Callable[..., T], str], 

262 *args: Any, 

263 **kwargs: Any, 

264 ) -> T: ... 

265 

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. 

275 

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 

279 

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 

285 

286 You can write 

287 

288 >>> ( 

289 ... df.groupby("group").pipe(f).pipe(g, arg1=1).pipe(h, arg2=2, arg3=3) 

290 ... ) # doctest: +SKIP 

291 

292 which is much more readable. 

293 

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`. 

305 

306 Returns 

307 ------- 

308 any 

309 The result of applying ``func`` to the Resampler object. 

310 

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. 

317 

318 Notes 

319 ----- 

320 See more `here 

321 <https://pandas.pydata.org/pandas-docs/stable/user_guide/groupby.html#piping-function-calls>`_ 

322 

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 

334 

335 To get the difference between each 2-day period's maximum and minimum 

336 value in one pass, you can do 

337 

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) 

344 

345 @final 

346 def aggregate(self, func=None, *args, **kwargs): 

347 """ 

348 Aggregate using one or more operations over the specified axis. 

349 

350 This method applies aggregation functions to resampled groups, enabling 

351 summary statistics to be computed for each time period. 

352 

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. 

358 

359 Accepted combinations are: 

360 

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`. 

369 

370 Returns 

371 ------- 

372 scalar, Series or DataFrame 

373 

374 The return can be: 

375 

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 

379 

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. 

388 

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)``. 

397 

398 `agg` is an alias for `aggregate`. Use the alias. 

399 

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. 

403 

404 A passed user-defined-function will be passed a Series for evaluation. 

405 

406 If ``func`` defines an index relabeling, ``axis`` must be ``0`` or ``index``. 

407 

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 

420 

421 >>> r = s.resample("2s") 

422 

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 

428 

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 

434 

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 

440 

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) 

451 

452 return result 

453 

454 agg = aggregate 

455 apply = aggregate 

456 

457 @final 

458 def transform(self, arg, *args, **kwargs): 

459 """ 

460 Call function producing a like-indexed Series on each group. 

461 

462 Return a Series with the transformed values. 

463 

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. 

470 

471 Returns 

472 ------- 

473 Series 

474 A Series with the transformed values, maintaining the same index as 

475 the original object. 

476 

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. 

482 

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 

490 

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 ) 

500 

501 def _downsample(self, how, **kwargs): 

502 raise AbstractMethodError(self) 

503 

504 def _upsample(self, f, limit: int | None = None, fill_value=None): 

505 raise AbstractMethodError(self) 

506 

507 def _gotitem(self, key, ndim: int, subset=None): 

508 """ 

509 Sub-classes to define. Return a sliced object. 

510 

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 

529 

530 grouped = get_groupby( 

531 subset, by=None, grouper=grouper, group_keys=self.group_keys 

532 ) 

533 return grouped 

534 

535 def _groupby_and_aggregate(self, how, *args, **kwargs): 

536 """ 

537 Re-evaluate the obj with a groupby aggregation. 

538 """ 

539 grouper = self._grouper 

540 

541 # Excludes `on` column when provided 

542 obj = self._obj_with_exclusions 

543 

544 grouped = get_groupby(obj, by=None, grouper=grouper, group_keys=self.group_keys) 

545 

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 

557 

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) 

563 

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 

571 

572 # we have a non-reducing function 

573 # try to evaluate 

574 result = grouped.apply(how, *args, **kwargs) 

575 

576 return self._wrap_result(result) 

577 

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 ) 

592 

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 

599 

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) 

605 

606 if self._timegrouper._arrow_dtype is not None: 

607 result.index = result.index.astype(self._timegrouper._arrow_dtype) 

608 

609 return result 

610 

611 @final 

612 def ffill(self, limit: int | None = None): 

613 """ 

614 Forward fill the values. 

615 

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. 

620 

621 Parameters 

622 ---------- 

623 limit : int, optional 

624 Limit of how many values to fill. 

625 

626 Returns 

627 ------- 

628 Series 

629 The resampled data with missing values filled forward. 

630 

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. 

635 

636 Examples 

637 -------- 

638 Here we only create a ``Series``. 

639 

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 

652 

653 Example for ``ffill`` with downsampling (we have fewer dates after resampling): 

654 

655 >>> ser.resample("MS").ffill() 

656 2023-01-01 1 

657 2023-02-01 3 

658 Freq: MS, dtype: int64 

659 

660 Example for ``ffill`` with upsampling (fill the new dates with 

661 the previous value): 

662 

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 

673 

674 With upsampling and limiting (only fill the first new date with the 

675 previous value): 

676 

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) 

689 

690 @final 

691 def nearest(self, limit: int | None = None): 

692 """ 

693 Resample by using the nearest value. 

694 

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. 

703 

704 Parameters 

705 ---------- 

706 limit : int, optional 

707 Limit of how many values to fill. 

708 

709 Returns 

710 ------- 

711 Series or DataFrame 

712 An upsampled Series or DataFrame with ``NaN`` values filled with 

713 their nearest value. 

714 

715 See Also 

716 -------- 

717 bfill : Backward fill the new missing values in the resampled data. 

718 ffill : Forward fill ``NaN`` values. 

719 

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 

727 

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 

735 

736 Limit the number of upsampled values imputed by the nearest: 

737 

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) 

747 

748 @final 

749 def bfill(self, limit: int | None = None): 

750 """ 

751 Backward fill the new missing values in the resampled data. 

752 

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. 

759 

760 Parameters 

761 ---------- 

762 limit : int, optional 

763 Limit of how many values to fill. 

764 

765 Returns 

766 ------- 

767 Series, DataFrame 

768 An upsampled Series or DataFrame with backward filled NaN values. 

769 

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'. 

778 

779 References 

780 ---------- 

781 .. [1] https://en.wikipedia.org/wiki/Imputation_%28statistics%29 

782 

783 Examples 

784 -------- 

785 Resampling a Series: 

786 

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 

795 

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 

803 

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 

815 

816 Resampling a DataFrame that has missing values: 

817 

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 

827 

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 

835 

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) 

849 

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. 

863 

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. 

868 

869 Parameters 

870 ---------- 

871 method : str, default 'linear' 

872 Interpolation technique to use. One of: 

873 

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`. 

894 

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. 

905 

906 limit_area : {`None`, 'inside', 'outside'}, default None 

907 Restrict which NaNs are filled based on their position relative to 

908 the valid values. 

909 

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). 

914 

915 **kwargs : optional 

916 Keyword arguments to pass on to the interpolating function. 

917 

918 Returns 

919 ------- 

920 DataFrame or Series 

921 Interpolated values at the specified freq. 

922 

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. 

930 

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. 

936 

937 Examples 

938 -------- 

939 

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 

950 

951 Downsample the dataframe to 0.5Hz by providing the period time of 2s. 

952 

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 

958 

959 Upsample the dataframe to 2Hz by providing the period time of 500ms. 

960 

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 

972 

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: 

978 

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 

992 

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.") 

1008 

1009 result = self._upsample("asfreq") 

1010 

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) 

1017 

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 ) 

1031 

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() 

1037 

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 ) 

1047 

1048 # No further steps if the original data has a PeriodIndex 

1049 if is_period_index: 

1050 return result_interpolated 

1051 

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] 

1055 

1056 # Make sure frequency indexes are preserved 

1057 result_interpolated.index = final_index 

1058 return result_interpolated 

1059 

1060 @final 

1061 def asfreq(self, fill_value=None): 

1062 """ 

1063 Return the values at the new freq, essentially a reindex. 

1064 

1065 This method selects data at the specified frequency without aggregation, 

1066 useful for upsampling to a higher frequency or selecting specific time points. 

1067 

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). 

1073 

1074 Returns 

1075 ------- 

1076 DataFrame or Series 

1077 Values at the specified freq. 

1078 

1079 See Also 

1080 -------- 

1081 Series.asfreq: Convert TimeSeries to specified frequency. 

1082 DataFrame.asfreq: Convert TimeSeries to specified frequency. 

1083 

1084 Examples 

1085 -------- 

1086 

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) 

1105 

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. 

1114 

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. 

1118 

1119 Parameters 

1120 ---------- 

1121 numeric_only : bool, default False 

1122 Include only float, int, boolean columns. 

1123 

1124 .. versionchanged:: 2.0.0 

1125 

1126 numeric_only no longer accepts ``None``. 

1127 

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. 

1131 

1132 Returns 

1133 ------- 

1134 Series or DataFrame 

1135 Computed sum of values within each group. 

1136 

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. 

1144 

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) 

1165 

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. 

1174 

1175 This method calculates the product of all values within each resampled 

1176 time period. 

1177 

1178 Parameters 

1179 ---------- 

1180 numeric_only : bool, default False 

1181 Include only float, int, boolean columns. 

1182 

1183 .. versionchanged:: 2.0.0 

1184 

1185 numeric_only no longer accepts ``None``. 

1186 

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. 

1190 

1191 Returns 

1192 ------- 

1193 Series or DataFrame 

1194 Computed prod of values within each group. 

1195 

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. 

1202 

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) 

1223 

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. 

1232 

1233 This method returns the minimum value within each resampled time period. 

1234 

1235 Parameters 

1236 ---------- 

1237 numeric_only : bool, default False 

1238 Include only float, int, boolean columns. 

1239 

1240 .. versionchanged:: 2.0.0 

1241 

1242 numeric_only no longer accepts ``None``. 

1243 

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. 

1247 

1248 Returns 

1249 ------- 

1250 Series or DataFrame 

1251 Compute the minimum value in the given Series or DataFrame. 

1252 

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. 

1259 

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) 

1280 

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. 

1289 

1290 This method returns the maximum value within each resampled time period. 

1291 

1292 Parameters 

1293 ---------- 

1294 numeric_only : bool, default False 

1295 Include only float, int, boolean columns. 

1296 

1297 .. versionchanged:: 2.0.0 

1298 

1299 numeric_only no longer accepts ``None``. 

1300 

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. 

1304 

1305 Returns 

1306 ------- 

1307 Series or DataFrame 

1308 Computes the maximum value in the given Series or Dataframe. 

1309 

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. 

1316 

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) 

1337 

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. 

1347 

1348 This method returns the first value encountered in each resampled time 

1349 period, skipping missing values by default. 

1350 

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. 

1360 

1361 Returns 

1362 ------- 

1363 Series or DataFrame 

1364 First values within each group. 

1365 

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. 

1370 

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 ) 

1393 

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. 

1403 

1404 This method returns the last value encountered in each resampled time 

1405 period, skipping missing values by default. 

1406 

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. 

1416 

1417 Returns 

1418 ------- 

1419 Series or DataFrame 

1420 Last of values within each group. 

1421 

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. 

1426 

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 ) 

1449 

1450 @final 

1451 def median(self, numeric_only: bool = False): 

1452 """ 

1453 Compute median of groups, excluding missing values. 

1454 

1455 For multiple groupings, the result index will be a MultiIndex 

1456 

1457 Parameters 

1458 ---------- 

1459 numeric_only : bool, default False 

1460 Include only float, int, boolean columns. 

1461 

1462 .. versionchanged:: 2.0.0 

1463 

1464 numeric_only no longer accepts ``None`` and defaults to False. 

1465 

1466 Returns 

1467 ------- 

1468 Series or DataFrame 

1469 Median of values within each group. 

1470 

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. 

1476 

1477 Examples 

1478 -------- 

1479 

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) 

1499 

1500 @final 

1501 def mean( 

1502 self, 

1503 numeric_only: bool = False, 

1504 ): 

1505 """ 

1506 Compute mean of groups, excluding missing values. 

1507 

1508 This method calculates the arithmetic mean of values within each 

1509 resampled time period. 

1510 

1511 Parameters 

1512 ---------- 

1513 numeric_only : bool, default False 

1514 Include only `float`, `int` or `boolean` data. 

1515 

1516 .. versionchanged:: 2.0.0 

1517 

1518 numeric_only now defaults to ``False``. 

1519 

1520 Returns 

1521 ------- 

1522 DataFrame or Series 

1523 Mean of values within each group. 

1524 

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. 

1534 

1535 Examples 

1536 -------- 

1537 

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) 

1556 

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. 

1565 

1566 This method calculates the sample standard deviation of values within 

1567 each resampled time period. 

1568 

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. 

1575 

1576 .. versionchanged:: 2.0.0 

1577 

1578 numeric_only now defaults to ``False``. 

1579 

1580 Returns 

1581 ------- 

1582 DataFrame or Series 

1583 Standard deviation of values within each group. 

1584 

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. 

1592 

1593 Notes 

1594 ----- 

1595 To use the same divisor as ``numpy.std``, use ``ddof=0`` instead of 

1596 the default ``ddof=1``. 

1597 

1598 Examples 

1599 -------- 

1600 

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) 

1620 

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. 

1629 

1630 This method calculates the sample variance of values within each 

1631 resampled time period. 

1632 

1633 Parameters 

1634 ---------- 

1635 ddof : int, default 1 

1636 Degrees of freedom. 

1637 

1638 numeric_only : bool, default False 

1639 Include only `float`, `int` or `boolean` data. 

1640 

1641 .. versionchanged:: 2.0.0 

1642 

1643 numeric_only now defaults to ``False``. 

1644 

1645 Returns 

1646 ------- 

1647 DataFrame or Series 

1648 Variance of values within each group. 

1649 

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. 

1657 

1658 Examples 

1659 -------- 

1660 

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 

1678 

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) 

1685 

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. 

1694 

1695 For multiple groupings, the result index will be a MultiIndex. 

1696 

1697 Parameters 

1698 ---------- 

1699 ddof : int, default 1 

1700 Degrees of freedom. 

1701 

1702 numeric_only : bool, default False 

1703 Include only `float`, `int` or `boolean` data. 

1704 

1705 .. versionchanged:: 2.0.0 

1706 

1707 numeric_only now defaults to ``False``. 

1708 

1709 Returns 

1710 ------- 

1711 Series or DataFrame 

1712 Standard error of the mean of values within each group. 

1713 

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. 

1718 

1719 Examples 

1720 -------- 

1721 

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) 

1741 

1742 @final 

1743 def ohlc(self): 

1744 """ 

1745 Compute open, high, low and close values of a group, excluding missing values. 

1746 

1747 This method computes OHLC (Open-High-Low-Close) values, commonly used 

1748 in financial data analysis to summarize price movements within time periods. 

1749 

1750 Returns 

1751 ------- 

1752 DataFrame 

1753 Open, high, low and close values within each group. 

1754 

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. 

1760 

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 

1796 

1797 return self._downsample("ohlc") 

1798 

1799 @final 

1800 def nunique(self): 

1801 """ 

1802 Return number of unique elements in the group. 

1803 

1804 This method counts the number of distinct values within each resampled 

1805 time period. 

1806 

1807 Returns 

1808 ------- 

1809 Series 

1810 Number of unique values within each group. 

1811 

1812 See Also 

1813 -------- 

1814 core.groupby.SeriesGroupBy.nunique : Method nunique for SeriesGroupBy. 

1815 

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") 

1836 

1837 @final 

1838 def size(self): 

1839 """ 

1840 Compute group sizes. 

1841 

1842 This method returns the number of rows in each resampled time period, 

1843 including rows with missing values. 

1844 

1845 Returns 

1846 ------- 

1847 Series 

1848 Number of rows in each group. 

1849 

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. 

1855 

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") 

1873 

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() 

1878 

1879 if not len(self.ax): 

1880 from pandas import Series 

1881 

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 

1888 

1889 @final 

1890 def count(self): 

1891 """ 

1892 Compute count of group, excluding missing values. 

1893 

1894 This method returns the number of non-null values in each resampled 

1895 time period. 

1896 

1897 Returns 

1898 ------- 

1899 Series or DataFrame 

1900 Count of values within each group. 

1901 

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. 

1907 

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 

1935 

1936 result = DataFrame( 

1937 [], index=result.index, columns=result.columns, dtype="int64" 

1938 ) 

1939 

1940 return result 

1941 

1942 @final 

1943 def quantile(self, q: float | list[float] | AnyArrayLike = 0.5, **kwargs): 

1944 """ 

1945 Return value at the given quantile. 

1946 

1947 Computes the quantile of values within each resampled group. 

1948 

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. 

1955 

1956 Returns 

1957 ------- 

1958 DataFrame or Series 

1959 Quantile of values within each group. 

1960 

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. 

1971 

1972 Examples 

1973 -------- 

1974 

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 

1992 

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) 

1999 

2000 

2001class _GroupByMixin(PandasObject, SelectionMixin): 

2002 """ 

2003 Provide the groupby facilities. 

2004 """ 

2005 

2006 _attributes: list[str] # in practice the same as Resampler._attributes 

2007 _selection: IndexLabel | None = None 

2008 _groupby: GroupBy 

2009 _timegrouper: TimeGrouper 

2010 

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 

2020 

2021 assert isinstance(groupby, GroupBy), type(groupby) 

2022 

2023 # parent is always a Resampler, sometimes a _GroupByMixin 

2024 assert isinstance(parent, Resampler), type(parent) 

2025 

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 

2031 

2032 self.binner = parent.binner 

2033 self.key = key 

2034 

2035 self._groupby = groupby 

2036 self._timegrouper = copy.copy(parent._timegrouper) 

2037 

2038 self.ax = parent.ax 

2039 self.obj = parent.obj 

2040 

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 """ 

2047 

2048 def func(x): 

2049 x = self._resampler_cls(x, timegrouper=self._timegrouper, gpr_index=self.ax) 

2050 

2051 if isinstance(f, str): 

2052 return getattr(x, f)(**kwargs) 

2053 

2054 return x.apply(f, *args, **kwargs) 

2055 

2056 result = self._groupby.apply(func) 

2057 

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 ) 

2067 

2068 return self._wrap_result(result) 

2069 

2070 _upsample = _apply 

2071 _downsample = _apply 

2072 _groupby_and_aggregate = _apply 

2073 

2074 @final 

2075 def _gotitem(self, key, ndim, subset=None): 

2076 """ 

2077 Sub-classes to define. Return a sliced object. 

2078 

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 

2095 

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 

2103 

2104 selection = self._infer_selection(key, subset) 

2105 

2106 new_rs = type(self)( 

2107 groupby=groupby, 

2108 parent=cast("Resampler", self), 

2109 selection=selection, 

2110 ) 

2111 return new_rs 

2112 

2113 

2114class DatetimeIndexResampler(Resampler): 

2115 ax: DatetimeIndex 

2116 

2117 @property 

2118 def _resampler_for_grouping(self) -> type[DatetimeIndexResamplerGroupBy]: 

2119 return DatetimeIndexResamplerGroupBy 

2120 

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) 

2124 

2125 def _downsample(self, how, **kwargs): 

2126 """ 

2127 Downsample the cython defined function. 

2128 

2129 Parameters 

2130 ---------- 

2131 how : string / cython mapped function 

2132 **kwargs : kw args passed to how function 

2133 """ 

2134 ax = self.ax 

2135 

2136 # Excludes `on` column when provided 

2137 obj = self._obj_with_exclusions 

2138 

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 

2145 

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) 

2150 

2151 def _adjust_binner_for_upsample(self, binner): 

2152 """ 

2153 Adjust our binner when upsampling. 

2154 

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 

2162 

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 ) 

2180 

2181 ax = self.ax 

2182 obj = self._selected_obj 

2183 binner = self.binner 

2184 res_index = self._adjust_binner_for_upsample(binner) 

2185 

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 ) 

2201 

2202 return self._wrap_result(result) 

2203 

2204 def _wrap_result(self, result): 

2205 result = super()._wrap_result(result) 

2206 

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 

2220 

2221 

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 """ 

2231 

2232 @property 

2233 def _resampler_cls(self): 

2234 return DatetimeIndexResampler 

2235 

2236 

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] 

2241 

2242 @property 

2243 def _resampler_for_grouping(self): 

2244 return PeriodIndexResamplerGroupBy 

2245 

2246 def _get_binner_for_time(self): 

2247 return self._timegrouper._get_period_bins(self.ax) 

2248 

2249 def _convert_obj(self, obj: NDFrameT) -> NDFrameT: 

2250 obj = super()._convert_obj(obj) 

2251 

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) 

2260 

2261 return obj 

2262 

2263 def _downsample(self, how, **kwargs): 

2264 """ 

2265 Downsample the cython defined function. 

2266 

2267 Parameters 

2268 ---------- 

2269 how : string / cython mapped function 

2270 **kwargs : kw args passed to how function 

2271 """ 

2272 ax = self.ax 

2273 

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 ) 

2283 

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 

2289 

2290 return self._groupby_and_aggregate(how, **kwargs) 

2291 

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 

2306 

2307 # Start vs. end of period 

2308 memb = ax.asfreq(self.freq, how=self.convention) 

2309 

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) 

2320 

2321 

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 """ 

2331 

2332 @property 

2333 def _resampler_cls(self): 

2334 return PeriodIndexResampler 

2335 

2336 

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] 

2341 

2342 @property 

2343 def _resampler_for_grouping(self): 

2344 return TimedeltaIndexResamplerGroupBy 

2345 

2346 def _get_binner_for_time(self): 

2347 return self._timegrouper._get_time_delta_bins(self.ax) 

2348 

2349 def _adjust_binner_for_upsample(self, binner): 

2350 """ 

2351 Adjust our binner when upsampling. 

2352 

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 

2357 

2358 

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 """ 

2368 

2369 @property 

2370 def _resampler_cls(self): 

2371 return TimedeltaIndexResampler 

2372 

2373 

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) 

2380 

2381 

2382get_resampler.__doc__ = Resampler.__doc__ 

2383 

2384 

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) 

2401 

2402 

2403@set_module("pandas.api.typing") 

2404class TimeGrouper(Grouper): 

2405 """ 

2406 Custom groupby class for time-interval grouping. 

2407 

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 """ 

2416 

2417 _attributes = ( 

2418 *Grouper._attributes, 

2419 "closed", 

2420 "label", 

2421 "how", 

2422 "convention", 

2423 "origin", 

2424 "offset", 

2425 ) 

2426 

2427 origin: TimeGrouperOrigin 

2428 

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`") 

2456 

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) 

2465 

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 ) 

2483 

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" 

2507 

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 

2516 

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 

2533 

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 

2541 

2542 # always sort time groupers 

2543 kwargs["sort"] = True 

2544 

2545 super().__init__(freq=freq, key=key, **kwargs) 

2546 

2547 def _get_resampler(self, obj: NDFrame) -> Resampler: 

2548 """ 

2549 Return my resampler or raise if we have an invalid axis. 

2550 

2551 Parameters 

2552 ---------- 

2553 obj : Series or DataFrame 

2554 

2555 Returns 

2556 ------- 

2557 Resampler 

2558 

2559 Raises 

2560 ------ 

2561 TypeError if incompatible axis 

2562 

2563 """ 

2564 _, ax, _ = self._set_grouper(obj, gpr_index=None) 

2565 

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 ) 

2588 

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 ) 

2610 

2611 raise TypeError( 

2612 "Only valid with DatetimeIndex, " 

2613 "TimedeltaIndex or PeriodIndex, " 

2614 f"but got an instance of '{type(ax).__name__}'" 

2615 ) 

2616 

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``. 

2629 

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) 

2637 

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 ) 

2644 

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 

2650 

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()] 

2656 

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 

2666 

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 ) 

2693 

2694 ax_values = ax.asi8 

2695 binner, bin_edges = self._adjust_bin_edges(binner, ax_values) 

2696 

2697 # general version, knowing nothing about relative frequencies 

2698 bins = lib.generate_bins_dt64(ax_values, bin_edges, self.closed, hasnans=False) 

2699 

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:] 

2706 

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) 

2713 

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)] 

2719 

2720 return binner, bins, labels 

2721 

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 

2726 

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 

2751 

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 

2759 

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 ) 

2766 

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 ) 

2773 

2774 if not len(ax): 

2775 binner = labels = TimedeltaIndex(data=[], freq=self.freq, name=ax.name) 

2776 return binner, [], labels 

2777 

2778 start, end = ax.min(), ax.max() 

2779 

2780 if self.closed == "right": 

2781 end += self.freq # type: ignore[operator] 

2782 

2783 labels = binner = timedelta_range( 

2784 start=start, end=end, freq=self.freq, name=ax.name 

2785 ) 

2786 

2787 end_stamps = labels 

2788 if self.closed == "left": 

2789 end_stamps += self.freq 

2790 

2791 bins = ax.searchsorted(end_stamps, side=self.closed) 

2792 

2793 if self.offset: 

2794 # GH 10530 & 31809 

2795 labels += self.offset 

2796 

2797 return binner, bins, labels 

2798 

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 ) 

2805 

2806 freq = self.freq 

2807 

2808 if len(ax) == 0: 

2809 binner = labels = PeriodIndex( 

2810 data=[], freq=freq, name=ax.name, dtype=ax.dtype 

2811 ) 

2812 return binner, [], labels 

2813 

2814 labels = binner = period_range(start=ax[0], end=ax[-1], freq=freq, name=ax.name) 

2815 

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") 

2820 

2821 return binner, bins, labels 

2822 

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 ) 

2829 

2830 memb = ax.asfreq(self.freq, how=self.convention) 

2831 

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] 

2839 

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 

2848 

2849 freq_mult = self.freq.n 

2850 

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 

2854 

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 ) 

2868 

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 

2874 

2875 labels = binner = period_range( 

2876 start=start, end=end, freq=self.freq, name=ax.name 

2877 ) 

2878 

2879 i8 = memb.asi8 

2880 

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 

2888 

2889 # Wrap in PeriodArray for PeriodArray.searchsorted 

2890 prng = type(memb._data)(rng, dtype=memb.dtype) 

2891 bins = memb.searchsorted(prng, side="left") 

2892 

2893 if nat_count > 0: 

2894 binner, bins, labels = _insert_nat_bin(binner, bins, labels, nat_count) 

2895 

2896 return binner, bins, labels 

2897 

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 

2909 

2910 

2911@overload 

2912def _take_new_index( 

2913 obj: DataFrame, indexer: npt.NDArray[np.intp], new_index: Index 

2914) -> DataFrame: ... 

2915 

2916 

2917@overload 

2918def _take_new_index( 

2919 obj: Series, indexer: npt.NDArray[np.intp], new_index: Index 

2920) -> Series: ... 

2921 

2922 

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") 

2936 

2937 

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. 

2953 

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: 

2968 

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. 

2974 

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) 

2987 

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) 

2992 

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() 

3005 

3006 if closed == "left": 

3007 first = Timestamp(freq.rollback(first)) 

3008 else: 

3009 first = Timestamp(first - freq) 

3010 

3011 last = Timestamp(last + freq) 

3012 

3013 return first, last 

3014 

3015 

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. 

3027 

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. 

3041 

3042 If a timestamp is not used, these values are also supported: 

3043 

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. 

3049 

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") 

3056 

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) 

3062 

3063 first_ts, last_ts = _get_timestamp_range_edges( 

3064 first_ts, last_ts, freq, unit="ns", closed=closed, origin=origin, offset=offset 

3065 ) 

3066 

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 

3070 

3071 

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) 

3080 

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 

3088 

3089 

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) 

3108 

3109 freq_value = Timedelta(freq).as_unit(unit)._value 

3110 

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 

3128 

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") 

3138 

3139 foffset = (first._value - origin_timestamp) % freq_value 

3140 loffset = (last._value - origin_timestamp) % freq_value 

3141 

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 

3148 

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 

3161 

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 

3170 

3171 

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. 

3182 

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") 

3188 

3189 if how is None: 

3190 how = "E" 

3191 

3192 if isinstance(freq, BaseOffset): 

3193 if hasattr(freq, "_period_dtype_code"): 

3194 freq = PeriodDtype(freq)._freqstr 

3195 

3196 new_obj = obj.copy() 

3197 new_obj.index = obj.index.asfreq(freq, how=how) 

3198 

3199 elif len(obj.index) == 0: 

3200 new_obj = obj.copy() 

3201 

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() 

3213 

3214 return new_obj 

3215 

3216 

3217def _asfreq_compat(index: FreqIndexT, freq) -> FreqIndexT: 

3218 """ 

3219 Helper to mimic asfreq on (empty) DatetimeIndex and TimedeltaIndex. 

3220 

3221 Parameters 

3222 ---------- 

3223 index : PeriodIndex, DatetimeIndex, or TimedeltaIndex 

3224 freq : DateOffset 

3225 

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