Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pandas/tseries/frequencies.py: 21%

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

330 statements  

1from __future__ import annotations 

2 

3from typing import TYPE_CHECKING 

4import warnings 

5 

6import numpy as np 

7 

8from pandas._config import using_infer_freq_offset 

9 

10from pandas._libs import lib 

11from pandas._libs.algos import unique_deltas 

12from pandas._libs.tslibs import ( 

13 Timestamp, 

14 get_unit_from_dtype, 

15 periods_per_day, 

16 tz_convert_from_utc, 

17) 

18from pandas._libs.tslibs.ccalendar import ( 

19 MONTH_ALIASES, 

20 MONTH_NUMBERS, 

21 int_to_weekday, 

22) 

23from pandas._libs.tslibs.dtypes import OFFSET_TO_PERIOD_FREQSTR 

24from pandas._libs.tslibs.fields import ( 

25 build_field_sarray, 

26 month_position_check, 

27) 

28from pandas._libs.tslibs.offsets import ( 

29 BaseOffset, 

30 Day, 

31 to_offset, 

32) 

33from pandas._libs.tslibs.parsing import get_rule_month 

34from pandas.errors import Pandas4Warning 

35from pandas.util._decorators import ( 

36 cache_readonly, 

37 set_module, 

38) 

39from pandas.util._exceptions import find_stack_level 

40 

41from pandas.core.dtypes.common import is_numeric_dtype 

42from pandas.core.dtypes.dtypes import ( 

43 ArrowDtype, 

44 DatetimeTZDtype, 

45 PeriodDtype, 

46) 

47from pandas.core.dtypes.generic import ( 

48 ABCIndex, 

49 ABCSeries, 

50) 

51 

52if TYPE_CHECKING: 

53 from pandas._typing import npt 

54 

55 from pandas import ( 

56 DatetimeIndex, 

57 Series, 

58 TimedeltaIndex, 

59 ) 

60 from pandas.core.arrays.datetimelike import DatetimeLikeArrayMixin 

61# -------------------------------------------------------------------- 

62# Offset related functions 

63 

64 

65def get_period_alias(offset_str: str) -> str | None: 

66 """ 

67 Alias to closest period strings BQ->Q etc. 

68 """ 

69 return OFFSET_TO_PERIOD_FREQSTR.get(offset_str, None) 

70 

71 

72def maybe_convert_inferred_freq( 

73 freq_str: str | None, caller: str, expr: str | None = None 

74) -> str | BaseOffset | None: 

75 """ 

76 Implement the GH#55504 deprecation of inferred frequencies being returned 

77 as strings rather than BaseOffset objects. 

78 

79 Parameters 

80 ---------- 

81 freq_str : str or None 

82 The inferred frequency, as returned by infer_freq_str. 

83 caller : str 

84 User-facing name of the attribute or function being called, used in 

85 the warning message. 

86 expr : str, optional 

87 Expression the user can append ``.freqstr`` to in order to keep the 

88 string result. Defaults to `caller`. 

89 

90 Returns 

91 ------- 

92 str, BaseOffset, or None 

93 """ 

94 if freq_str is None: 

95 # The result is None either way, so there is no behavior change 

96 # to warn about. 

97 return None 

98 

99 opt = using_infer_freq_offset() 

100 if opt is True: 

101 return to_offset(freq_str) 

102 if opt is None: 

103 warnings.warn( 

104 f"A future version of pandas will return a BaseOffset object " 

105 f"instead of a string from {caller}. " 

106 f"Use pd.set_option('future.infer_freq_returns_offset', True) " 

107 f"to get the future behavior, or set to False to keep the old " 

108 f"behavior and silence this warning. To preserve the string " 

109 f"representation, use ``{expr or caller}.freqstr``.", 

110 Pandas4Warning, 

111 stacklevel=find_stack_level(), 

112 ) 

113 return freq_str 

114 

115 

116# --------------------------------------------------------------------- 

117# Period codes 

118 

119 

120def infer_freq_str( 

121 index: DatetimeIndex | TimedeltaIndex | Series | DatetimeLikeArrayMixin, 

122) -> str | None: 

123 """ 

124 Internal version of infer_freq that returns a string without 

125 emitting a deprecation warning. 

126 """ 

127 from pandas.core.api import DatetimeIndex 

128 

129 if isinstance(index, ABCSeries): 

130 values = index._values 

131 

132 if isinstance(index.dtype, ArrowDtype): 

133 import pyarrow as pa 

134 

135 if pa.types.is_timestamp(values.dtype.pyarrow_dtype): 

136 # GH#58403 

137 values = values._to_datetimearray() 

138 

139 if not ( 

140 lib.is_np_dtype(values.dtype, "mM") 

141 or isinstance(values.dtype, DatetimeTZDtype) 

142 or values.dtype == object 

143 ): 

144 raise TypeError( 

145 "cannot infer freq from a non-convertible dtype " 

146 f"on a Series of {index.dtype}" 

147 ) 

148 index = values 

149 

150 inferer: _FrequencyInferer 

151 

152 if not hasattr(index, "dtype"): 

153 pass 

154 elif isinstance(index.dtype, PeriodDtype): 

155 raise TypeError( 

156 "PeriodIndex given. Check the `freq` attribute instead of using infer_freq." 

157 ) 

158 elif lib.is_np_dtype(index.dtype, "m"): 

159 # Allow TimedeltaIndex and TimedeltaArray 

160 inferer = _TimedeltaFrequencyInferer(index) 

161 return inferer.get_freq() 

162 

163 elif is_numeric_dtype(index.dtype): 

164 raise TypeError( 

165 f"cannot infer freq from a non-convertible index of dtype {index.dtype}" 

166 ) 

167 

168 if not isinstance(index, DatetimeIndex): 

169 index = DatetimeIndex(index, copy=False) 

170 

171 inferer = _FrequencyInferer(index) 

172 return inferer.get_freq() 

173 

174 

175@set_module("pandas") 

176def infer_freq( 

177 index: DatetimeIndex | TimedeltaIndex | Series | DatetimeLikeArrayMixin, 

178) -> str | BaseOffset | None: 

179 """ 

180 Infer the most likely frequency given the input index. 

181 

182 .. deprecated:: 3.1.0 

183 A future version of pandas will return a :class:`BaseOffset` instead of 

184 a string. Use 

185 ``pd.set_option('future.infer_freq_returns_offset', True)`` to opt in 

186 to the future behavior. 

187 

188 This method attempts to deduce the most probable frequency (e.g., 'D' for daily, 

189 'H' for hourly) from a sequence of datetime-like objects. It is particularly useful 

190 when the frequency of a time series is not explicitly set or known but can be 

191 inferred from its values. 

192 

193 Parameters 

194 ---------- 

195 index : DatetimeIndex, TimedeltaIndex, Series or array-like 

196 If passed a Series will use the values of the series (NOT THE INDEX). 

197 

198 Returns 

199 ------- 

200 str or None 

201 None if no discernible frequency. 

202 

203 Raises 

204 ------ 

205 TypeError 

206 If the index is not datetime-like. 

207 ValueError 

208 If there are fewer than three values. 

209 

210 See Also 

211 -------- 

212 date_range : Return a fixed frequency DatetimeIndex. 

213 timedelta_range : Return a fixed frequency TimedeltaIndex with day as the default. 

214 period_range : Return a fixed frequency PeriodIndex. 

215 DatetimeIndex.freq : Return the frequency object if it is set, otherwise None. 

216 

217 Examples 

218 -------- 

219 >>> idx = pd.date_range(start="2020/12/01", end="2020/12/30", periods=30) 

220 >>> pd.infer_freq(idx) # doctest: +SKIP 

221 'D' 

222 """ 

223 return maybe_convert_inferred_freq( 

224 infer_freq_str(index), "infer_freq", expr="infer_freq(...)" 

225 ) 

226 

227 

228class _FrequencyInferer: 

229 """ 

230 Not sure if I can avoid the state machine here 

231 """ 

232 

233 def __init__(self, index) -> None: 

234 self.index = index 

235 self.i8values = index.asi8 

236 

237 # For get_unit_from_dtype we need the dtype to the underlying ndarray, 

238 # which for tz-aware is not the same as index.dtype 

239 if isinstance(index, ABCIndex): 

240 # error: Item "ndarray[Any, Any]" of "Union[ExtensionArray, 

241 # ndarray[Any, Any]]" has no attribute "_ndarray" 

242 self._creso = get_unit_from_dtype( 

243 index._data._ndarray.dtype # type: ignore[union-attr] 

244 ) 

245 else: 

246 # otherwise we have DTA/TDA 

247 self._creso = get_unit_from_dtype(index._ndarray.dtype) 

248 

249 # This moves the values, which are implicitly in UTC, to the 

250 # the timezone so they are in local time 

251 if hasattr(index, "tz"): 

252 if index.tz is not None: 

253 self.i8values = tz_convert_from_utc( 

254 self.i8values, index.tz, reso=self._creso 

255 ) 

256 

257 if len(index) < 3: 

258 raise ValueError("Need at least 3 dates to infer frequency") 

259 

260 self.is_monotonic = ( 

261 self.index._is_monotonic_increasing or self.index._is_monotonic_decreasing 

262 ) 

263 

264 @cache_readonly 

265 def deltas(self) -> npt.NDArray[np.int64]: 

266 return unique_deltas(self.i8values) 

267 

268 @cache_readonly 

269 def deltas_asi8(self) -> npt.NDArray[np.int64]: 

270 # NB: we cannot use self.i8values here because we may have converted 

271 # the tz in __init__ 

272 return unique_deltas(self.index.asi8) 

273 

274 @cache_readonly 

275 def is_unique(self) -> bool: 

276 return len(self.deltas) == 1 

277 

278 @cache_readonly 

279 def is_unique_asi8(self) -> bool: 

280 return len(self.deltas_asi8) == 1 

281 

282 def get_freq(self) -> str | None: 

283 """ 

284 Find the appropriate frequency string to describe the inferred 

285 frequency of self.i8values 

286 

287 Returns 

288 ------- 

289 str or None 

290 """ 

291 if not self.is_monotonic or not self.index._is_unique: 

292 return None 

293 

294 delta = self.deltas[0] 

295 ppd = periods_per_day(self._creso) 

296 if delta and _is_multiple(delta, ppd): 

297 return self._infer_daily_rule() 

298 

299 # Business hourly, maybe. 17: one day / 65: one weekend 

300 if self.hour_deltas in ([1, 17], [1, 65], [1, 17, 65]): 

301 return "bh" 

302 

303 # Possibly intraday frequency. Here we use the 

304 # original .asi8 values as the modified values 

305 # will not work around DST transitions. See #8772 

306 if not self.is_unique_asi8: 

307 return None 

308 

309 delta = self.deltas_asi8[0] 

310 pph = ppd // 24 

311 ppm = pph // 60 

312 pps = ppm // 60 

313 if _is_multiple(delta, pph): 

314 # Hours 

315 return _maybe_add_count("h", delta / pph) 

316 elif _is_multiple(delta, ppm): 

317 # Minutes 

318 return _maybe_add_count("min", delta / ppm) 

319 elif _is_multiple(delta, pps): 

320 # Seconds 

321 return _maybe_add_count("s", delta / pps) 

322 elif _is_multiple(delta, (pps // 1000)): 

323 # Milliseconds 

324 return _maybe_add_count("ms", delta / (pps // 1000)) 

325 elif _is_multiple(delta, (pps // 1_000_000)): 

326 # Microseconds 

327 return _maybe_add_count("us", delta / (pps // 1_000_000)) 

328 else: 

329 # Nanoseconds 

330 return _maybe_add_count("ns", delta) 

331 

332 @cache_readonly 

333 def day_deltas(self) -> list[float]: 

334 # NB: must be true division; flooring would let a fractional gap 

335 # match the exact [1, 3] check in _is_business_daily 

336 ppd = periods_per_day(self._creso) 

337 return [x / ppd for x in self.deltas] 

338 

339 @cache_readonly 

340 def hour_deltas(self) -> list[float]: 

341 # NB: must be true division, as in day_deltas; this one feeds the 

342 # exact [1, 17]/[1, 65] check in get_freq 

343 pph = periods_per_day(self._creso) // 24 

344 return [x / pph for x in self.deltas] 

345 

346 @cache_readonly 

347 def fields(self) -> np.ndarray: # structured array of fields 

348 return build_field_sarray(self.i8values, reso=self._creso) 

349 

350 @cache_readonly 

351 def rep_stamp(self) -> Timestamp: 

352 # creating a Timestamp preserving the i8 unit 

353 return Timestamp._from_value_and_reso(self.i8values[0], self._creso, None) 

354 

355 def month_position_check(self) -> str | None: 

356 return month_position_check(self.fields, self.index.day_of_week) 

357 

358 @cache_readonly 

359 def mdiffs(self) -> npt.NDArray[np.int64]: 

360 nmonths = self.fields["Y"] * 12 + self.fields["M"] 

361 return unique_deltas(nmonths.astype("i8")) 

362 

363 @cache_readonly 

364 def ydiffs(self) -> npt.NDArray[np.int64]: 

365 return unique_deltas(self.fields["Y"].astype("i8")) 

366 

367 def _infer_daily_rule(self) -> str | None: 

368 annual_rule = self._get_annual_rule() 

369 if annual_rule: 

370 nyears = self.ydiffs[0] 

371 month = MONTH_ALIASES[self.rep_stamp.month] 

372 alias = f"{annual_rule}-{month}" 

373 return _maybe_add_count(alias, nyears) 

374 

375 quarterly_rule = self._get_quarterly_rule() 

376 if quarterly_rule: 

377 nquarters = self.mdiffs[0] / 3 

378 if quarterly_rule in ("QS", "BQS"): 

379 # GH#36939 the anchor month is only known up to mod 3; use the 

380 # first-calendar-quarter representative so that to_period on 

381 # e.g. Jan/Apr/Jul/Oct starts (QS-JAN) keeps the calendar 

382 # convention Q-DEC 

383 mod_dict = {1: 1, 2: 2, 0: 3} 

384 else: 

385 mod_dict = {0: 12, 2: 11, 1: 10} 

386 month = MONTH_ALIASES[mod_dict[self.rep_stamp.month % 3]] 

387 alias = f"{quarterly_rule}-{month}" 

388 return _maybe_add_count(alias, nquarters) 

389 

390 monthly_rule = self._get_monthly_rule() 

391 if monthly_rule: 

392 return _maybe_add_count(monthly_rule, self.mdiffs[0]) 

393 

394 if self.is_unique: 

395 return self._get_daily_rule() 

396 

397 if self._is_business_daily(): 

398 return "B" 

399 

400 wom_rule = self._get_wom_rule() 

401 if wom_rule: 

402 return wom_rule 

403 

404 return None 

405 

406 def _get_daily_rule(self) -> str | None: 

407 ppd = periods_per_day(self._creso) 

408 days = self.deltas[0] / ppd 

409 if days % 7 == 0: 

410 # Weekly 

411 wd = int_to_weekday[self.rep_stamp.weekday()] 

412 alias = f"W-{wd}" 

413 return _maybe_add_count(alias, days / 7) 

414 else: 

415 return _maybe_add_count("D", days) 

416 

417 def _get_annual_rule(self) -> str | None: 

418 if len(self.ydiffs) > 1: 

419 return None 

420 

421 if len(np.unique(self.fields["M"])) > 1: 

422 return None 

423 

424 pos_check = self.month_position_check() 

425 

426 if pos_check is None: 

427 return None 

428 else: 

429 return {"cs": "YS", "bs": "BYS", "ce": "YE", "be": "BYE"}.get(pos_check) 

430 

431 def _get_quarterly_rule(self) -> str | None: 

432 if len(self.mdiffs) > 1: 

433 return None 

434 

435 if not self.mdiffs[0] % 3 == 0: 

436 return None 

437 

438 pos_check = self.month_position_check() 

439 

440 if pos_check is None: 

441 return None 

442 else: 

443 return {"cs": "QS", "bs": "BQS", "ce": "QE", "be": "BQE"}.get(pos_check) 

444 

445 def _get_monthly_rule(self) -> str | None: 

446 if len(self.mdiffs) > 1: 

447 return None 

448 pos_check = self.month_position_check() 

449 

450 if pos_check is None: 

451 return None 

452 else: 

453 return {"cs": "MS", "bs": "BMS", "ce": "ME", "be": "BME"}.get(pos_check) 

454 

455 def _is_business_daily(self) -> bool: 

456 # quick check: cannot be business daily 

457 if self.day_deltas != [1, 3]: 

458 return False 

459 

460 # probably business daily, but need to confirm 

461 first_weekday = self.index[0].weekday() 

462 shifts = np.diff(self.i8values) 

463 ppd = periods_per_day(self._creso) 

464 shifts = np.floor_divide(shifts, ppd) 

465 weekdays = np.mod(first_weekday + np.cumsum(shifts), 7) 

466 

467 return bool( 

468 np.all( 

469 ((weekdays == 0) & (shifts == 3)) 

470 | ((weekdays > 0) & (weekdays <= 4) & (shifts == 1)) 

471 ) 

472 ) 

473 

474 def _get_wom_rule(self) -> str | None: 

475 weekdays = np.unique(self.index.day_of_week) 

476 if len(weekdays) > 1: 

477 return None 

478 

479 week_of_months = np.unique((self.index.day - 1) // 7) 

480 # Only attempt to infer up to WOM-4. See #9425 

481 week_of_months = week_of_months[week_of_months < 4] 

482 if len(week_of_months) == 0 or len(week_of_months) > 1: 

483 return None 

484 

485 # get which week 

486 week = week_of_months[0] + 1 

487 wd = int_to_weekday[weekdays[0]] 

488 

489 return f"WOM-{week}{wd}" 

490 

491 

492class _TimedeltaFrequencyInferer(_FrequencyInferer): 

493 def _infer_daily_rule(self): 

494 if self.is_unique: 

495 return self._get_daily_rule() 

496 

497 

498def _is_multiple(us, mult: int) -> bool: 

499 return us % mult == 0 

500 

501 

502def _maybe_add_count(base: str, count: float) -> str: 

503 if count != 1: 

504 assert count == int(count) 

505 count = int(count) 

506 return f"{count}{base}" 

507 else: 

508 return base 

509 

510 

511# ---------------------------------------------------------------------- 

512# Frequency comparison 

513 

514 

515def is_subperiod(source, target) -> bool: 

516 """ 

517 Returns True if downsampling is possible between source and target 

518 frequencies 

519 

520 Parameters 

521 ---------- 

522 source : str or DateOffset 

523 Frequency converting from 

524 target : str or DateOffset 

525 Frequency converting to 

526 

527 Returns 

528 ------- 

529 bool 

530 """ 

531 if target is None or source is None: 

532 return False 

533 source = _maybe_coerce_freq(source) 

534 target = _maybe_coerce_freq(target) 

535 

536 if _is_annual(target): 

537 if _is_annual(source): 

538 return get_rule_month(source) == get_rule_month(target) 

539 if _is_quarterly(source): 

540 return _quarter_months_conform( 

541 get_rule_month(source), get_rule_month(target) 

542 ) 

543 return source in {"D", "C", "B", "M", "h", "min", "s", "ms", "us", "ns"} 

544 elif _is_quarterly(target): 

545 if _is_quarterly(source): 

546 return _quarter_months_conform( 

547 get_rule_month(source), get_rule_month(target) 

548 ) 

549 return source in {"D", "C", "B", "M", "h", "min", "s", "ms", "us", "ns"} 

550 elif _is_monthly(target): 

551 return source in {target, "D", "C", "B", "h", "min", "s", "ms", "us", "ns"} 

552 elif _is_weekly(target): 

553 return source in {target, "D", "C", "B", "h", "min", "s", "ms", "us", "ns"} 

554 elif target == "B": 

555 return source in {"B", "h", "min", "s", "ms", "us", "ns"} 

556 elif target == "C": 

557 return source in {"C", "h", "min", "s", "ms", "us", "ns"} 

558 elif target == "D": 

559 return source in {"D", "h", "min", "s", "ms", "us", "ns"} 

560 elif target == "h": 

561 return source in {"h", "min", "s", "ms", "us", "ns"} 

562 elif target == "min": 

563 return source in {"min", "s", "ms", "us", "ns"} 

564 elif target == "s": 

565 return source in {"s", "ms", "us", "ns"} 

566 elif target == "ms": 

567 return source in {"ms", "us", "ns"} 

568 elif target == "us": 

569 return source in {"us", "ns"} 

570 elif target == "ns": 

571 return source in {"ns"} 

572 else: 

573 return False 

574 

575 

576def is_superperiod(source, target) -> bool: 

577 """ 

578 Returns True if upsampling is possible between source and target 

579 frequencies 

580 

581 Parameters 

582 ---------- 

583 source : str or DateOffset 

584 Frequency converting from 

585 target : str or DateOffset 

586 Frequency converting to 

587 

588 Returns 

589 ------- 

590 bool 

591 """ 

592 if target is None or source is None: 

593 return False 

594 source = _maybe_coerce_freq(source) 

595 target = _maybe_coerce_freq(target) 

596 

597 if _is_annual(source): 

598 if _is_annual(target): 

599 return get_rule_month(source) == get_rule_month(target) 

600 

601 if _is_quarterly(target): 

602 smonth = get_rule_month(source) 

603 tmonth = get_rule_month(target) 

604 return _quarter_months_conform(smonth, tmonth) 

605 return target in {"D", "C", "B", "M", "h", "min", "s", "ms", "us", "ns"} 

606 elif _is_quarterly(source): 

607 if _is_quarterly(target): 

608 return _quarter_months_conform( 

609 get_rule_month(source), get_rule_month(target) 

610 ) 

611 return target in {"D", "C", "B", "M", "h", "min", "s", "ms", "us", "ns"} 

612 elif _is_monthly(source): 

613 return target in {source, "D", "C", "B", "h", "min", "s", "ms", "us", "ns"} 

614 elif _is_weekly(source): 

615 return target in {source, "D", "C", "B", "h", "min", "s", "ms", "us", "ns"} 

616 elif source == "B": 

617 return target in {"D", "C", "B", "h", "min", "s", "ms", "us", "ns"} 

618 elif source == "C": 

619 return target in {"D", "C", "B", "h", "min", "s", "ms", "us", "ns"} 

620 elif source == "D": 

621 return target in {"D", "C", "B", "h", "min", "s", "ms", "us", "ns"} 

622 elif source == "h": 

623 return target in {"h", "min", "s", "ms", "us", "ns"} 

624 elif source == "min": 

625 return target in {"min", "s", "ms", "us", "ns"} 

626 elif source == "s": 

627 return target in {"s", "ms", "us", "ns"} 

628 elif source == "ms": 

629 return target in {"ms", "us", "ns"} 

630 elif source == "us": 

631 return target in {"us", "ns"} 

632 elif source == "ns": 

633 return target in {"ns"} 

634 else: 

635 return False 

636 

637 

638def _maybe_coerce_freq(code) -> str: 

639 """we might need to coerce a code to a rule_code 

640 and uppercase it 

641 

642 Parameters 

643 ---------- 

644 code : str or DateOffset 

645 Frequency converting from 

646 

647 Returns 

648 ------- 

649 str 

650 """ 

651 assert code is not None 

652 if isinstance(code, BaseOffset): 

653 code = PeriodDtype(to_offset(code.rule_code))._freqstr 

654 # Strip any leading multiplier digits (e.g. '12h' -> 'h') so that 

655 # is_subperiod / is_superperiod can compare base frequency codes. GH#50355 

656 code = code.lstrip("0123456789") 

657 if code in {"h", "min", "s", "ms", "us", "ns"}: 

658 return code 

659 else: 

660 return code.upper() 

661 

662 

663def _quarter_months_conform(source: str, target: str) -> bool: 

664 snum = MONTH_NUMBERS[source] 

665 tnum = MONTH_NUMBERS[target] 

666 return snum % 3 == tnum % 3 

667 

668 

669def _is_annual(rule: str) -> bool: 

670 rule = rule.upper() 

671 return rule == "Y" or rule.startswith("Y-") 

672 

673 

674def _is_quarterly(rule: str) -> bool: 

675 rule = rule.upper() 

676 return rule == "Q" or rule.startswith(("Q-", "BQ")) 

677 

678 

679def _is_monthly(rule: str) -> bool: 

680 rule = rule.upper() 

681 return rule in ("M", "BM") 

682 

683 

684def _is_weekly(rule: str) -> bool: 

685 rule = rule.upper() 

686 return rule == "W" or rule.startswith("W-") 

687 

688 

689__all__ = [ 

690 "Day", 

691 "get_period_alias", 

692 "infer_freq", 

693 "is_subperiod", 

694 "is_superperiod", 

695 "to_offset", 

696]