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

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

318 statements  

1from __future__ import annotations 

2 

3from typing import TYPE_CHECKING 

4 

5import numpy as np 

6 

7from pandas._libs import lib 

8from pandas._libs.algos import unique_deltas 

9from pandas._libs.tslibs import ( 

10 Timestamp, 

11 get_unit_from_dtype, 

12 periods_per_day, 

13 tz_convert_from_utc, 

14) 

15from pandas._libs.tslibs.ccalendar import ( 

16 DAYS, 

17 MONTH_ALIASES, 

18 MONTH_NUMBERS, 

19 MONTHS, 

20 int_to_weekday, 

21) 

22from pandas._libs.tslibs.dtypes import OFFSET_TO_PERIOD_FREQSTR 

23from pandas._libs.tslibs.fields import ( 

24 build_field_sarray, 

25 month_position_check, 

26) 

27from pandas._libs.tslibs.offsets import ( 

28 DateOffset, 

29 Day, 

30 to_offset, 

31) 

32from pandas._libs.tslibs.parsing import get_rule_month 

33from pandas.util._decorators import ( 

34 cache_readonly, 

35 set_module, 

36) 

37 

38from pandas.core.dtypes.common import is_numeric_dtype 

39from pandas.core.dtypes.dtypes import ( 

40 ArrowDtype, 

41 DatetimeTZDtype, 

42 PeriodDtype, 

43) 

44from pandas.core.dtypes.generic import ( 

45 ABCIndex, 

46 ABCSeries, 

47) 

48 

49from pandas.core.algorithms import unique 

50 

51if TYPE_CHECKING: 

52 from pandas._typing import npt 

53 

54 from pandas import ( 

55 DatetimeIndex, 

56 Series, 

57 TimedeltaIndex, 

58 ) 

59 from pandas.core.arrays.datetimelike import DatetimeLikeArrayMixin 

60# -------------------------------------------------------------------- 

61# Offset related functions 

62 

63_need_suffix = ["QS", "BQE", "BQS", "YS", "BYE", "BYS"] 

64 

65for _prefix in _need_suffix: 

66 for _m in MONTHS: 

67 key = f"{_prefix}-{_m}" 

68 OFFSET_TO_PERIOD_FREQSTR[key] = OFFSET_TO_PERIOD_FREQSTR[_prefix] 

69 

70for _prefix in ["Y", "Q"]: 

71 for _m in MONTHS: 

72 _alias = f"{_prefix}-{_m}" 

73 OFFSET_TO_PERIOD_FREQSTR[_alias] = _alias 

74 

75for _d in DAYS: 

76 OFFSET_TO_PERIOD_FREQSTR[f"W-{_d}"] = f"W-{_d}" 

77 

78 

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

80 """ 

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

82 """ 

83 return OFFSET_TO_PERIOD_FREQSTR.get(offset_str, None) 

84 

85 

86# --------------------------------------------------------------------- 

87# Period codes 

88 

89 

90@set_module("pandas") 

91def infer_freq( 

92 index: DatetimeIndex | TimedeltaIndex | Series | DatetimeLikeArrayMixin, 

93) -> str | None: 

94 """ 

95 Infer the most likely frequency given the input index. 

96 

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

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

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

100 inferred from its values. 

101 

102 Parameters 

103 ---------- 

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

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

106 

107 Returns 

108 ------- 

109 str or None 

110 None if no discernible frequency. 

111 

112 Raises 

113 ------ 

114 TypeError 

115 If the index is not datetime-like. 

116 ValueError 

117 If there are fewer than three values. 

118 

119 See Also 

120 -------- 

121 date_range : Return a fixed frequency DatetimeIndex. 

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

123 period_range : Return a fixed frequency PeriodIndex. 

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

125 

126 Examples 

127 -------- 

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

129 >>> pd.infer_freq(idx) 

130 'D' 

131 """ 

132 from pandas.core.api import DatetimeIndex 

133 

134 if isinstance(index, ABCSeries): 

135 values = index._values 

136 

137 if isinstance(index.dtype, ArrowDtype): 

138 import pyarrow as pa 

139 

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

141 # GH#58403 

142 values = values._to_datetimearray() 

143 

144 if not ( 

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

146 or isinstance(values.dtype, DatetimeTZDtype) 

147 or values.dtype == object 

148 ): 

149 raise TypeError( 

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

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

152 ) 

153 index = values 

154 

155 inferer: _FrequencyInferer 

156 

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

158 pass 

159 elif isinstance(index.dtype, PeriodDtype): 

160 raise TypeError( 

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

162 ) 

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

164 # Allow TimedeltaIndex and TimedeltaArray 

165 inferer = _TimedeltaFrequencyInferer(index) 

166 return inferer.get_freq() 

167 

168 elif is_numeric_dtype(index.dtype): 

169 raise TypeError( 

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

171 ) 

172 

173 if not isinstance(index, DatetimeIndex): 

174 index = DatetimeIndex(index, copy=False) 

175 

176 inferer = _FrequencyInferer(index) 

177 return inferer.get_freq() 

178 

179 

180class _FrequencyInferer: 

181 """ 

182 Not sure if I can avoid the state machine here 

183 """ 

184 

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

186 self.index = index 

187 self.i8values = index.asi8 

188 

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

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

191 if isinstance(index, ABCIndex): 

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

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

194 self._creso = get_unit_from_dtype( 

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

196 ) 

197 else: 

198 # otherwise we have DTA/TDA 

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

200 

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

202 # the timezone so they are in local time 

203 if hasattr(index, "tz"): 

204 if index.tz is not None: 

205 self.i8values = tz_convert_from_utc( 

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

207 ) 

208 

209 if len(index) < 3: 

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

211 

212 self.is_monotonic = ( 

213 self.index._is_monotonic_increasing or self.index._is_monotonic_decreasing 

214 ) 

215 

216 @cache_readonly 

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

218 return unique_deltas(self.i8values) 

219 

220 @cache_readonly 

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

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

223 # the tz in __init__ 

224 return unique_deltas(self.index.asi8) 

225 

226 @cache_readonly 

227 def is_unique(self) -> bool: 

228 return len(self.deltas) == 1 

229 

230 @cache_readonly 

231 def is_unique_asi8(self) -> bool: 

232 return len(self.deltas_asi8) == 1 

233 

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

235 """ 

236 Find the appropriate frequency string to describe the inferred 

237 frequency of self.i8values 

238 

239 Returns 

240 ------- 

241 str or None 

242 """ 

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

244 return None 

245 

246 delta = self.deltas[0] 

247 ppd = periods_per_day(self._creso) 

248 if delta and _is_multiple(delta, ppd): 

249 return self._infer_daily_rule() 

250 

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

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

253 return "bh" 

254 

255 # Possibly intraday frequency. Here we use the 

256 # original .asi8 values as the modified values 

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

258 if not self.is_unique_asi8: 

259 return None 

260 

261 delta = self.deltas_asi8[0] 

262 pph = ppd // 24 

263 ppm = pph // 60 

264 pps = ppm // 60 

265 if _is_multiple(delta, pph): 

266 # Hours 

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

268 elif _is_multiple(delta, ppm): 

269 # Minutes 

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

271 elif _is_multiple(delta, pps): 

272 # Seconds 

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

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

275 # Milliseconds 

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

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

278 # Microseconds 

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

280 else: 

281 # Nanoseconds 

282 return _maybe_add_count("ns", delta) 

283 

284 @cache_readonly 

285 def day_deltas(self) -> list[int]: 

286 ppd = periods_per_day(self._creso) 

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

288 

289 @cache_readonly 

290 def hour_deltas(self) -> list[int]: 

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

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

293 

294 @cache_readonly 

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

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

297 

298 @cache_readonly 

299 def rep_stamp(self) -> Timestamp: 

300 return Timestamp(self.i8values[0], unit=self.index.unit) 

301 

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

303 return month_position_check(self.fields, self.index.dayofweek) 

304 

305 @cache_readonly 

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

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

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

309 

310 @cache_readonly 

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

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

313 

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

315 annual_rule = self._get_annual_rule() 

316 if annual_rule: 

317 nyears = self.ydiffs[0] 

318 month = MONTH_ALIASES[self.rep_stamp.month] 

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

320 return _maybe_add_count(alias, nyears) 

321 

322 quarterly_rule = self._get_quarterly_rule() 

323 if quarterly_rule: 

324 nquarters = self.mdiffs[0] / 3 

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

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

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

328 return _maybe_add_count(alias, nquarters) 

329 

330 monthly_rule = self._get_monthly_rule() 

331 if monthly_rule: 

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

333 

334 if self.is_unique: 

335 return self._get_daily_rule() 

336 

337 if self._is_business_daily(): 

338 return "B" 

339 

340 wom_rule = self._get_wom_rule() 

341 if wom_rule: 

342 return wom_rule 

343 

344 return None 

345 

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

347 ppd = periods_per_day(self._creso) 

348 days = self.deltas[0] / ppd 

349 if days % 7 == 0: 

350 # Weekly 

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

352 alias = f"W-{wd}" 

353 return _maybe_add_count(alias, days / 7) 

354 else: 

355 return _maybe_add_count("D", days) 

356 

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

358 if len(self.ydiffs) > 1: 

359 return None 

360 

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

362 return None 

363 

364 pos_check = self.month_position_check() 

365 

366 if pos_check is None: 

367 return None 

368 else: 

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

370 

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

372 if len(self.mdiffs) > 1: 

373 return None 

374 

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

376 return None 

377 

378 pos_check = self.month_position_check() 

379 

380 if pos_check is None: 

381 return None 

382 else: 

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

384 

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

386 if len(self.mdiffs) > 1: 

387 return None 

388 pos_check = self.month_position_check() 

389 

390 if pos_check is None: 

391 return None 

392 else: 

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

394 

395 def _is_business_daily(self) -> bool: 

396 # quick check: cannot be business daily 

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

398 return False 

399 

400 # probably business daily, but need to confirm 

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

402 shifts = np.diff(self.i8values) 

403 ppd = periods_per_day(self._creso) 

404 shifts = np.floor_divide(shifts, ppd) 

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

406 

407 return bool( 

408 np.all( 

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

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

411 ) 

412 ) 

413 

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

415 weekdays = unique(self.index.weekday) 

416 if len(weekdays) > 1: 

417 return None 

418 

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

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

421 week_of_months = week_of_months[week_of_months < 4] 

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

423 return None 

424 

425 # get which week 

426 week = week_of_months[0] + 1 

427 wd = int_to_weekday[weekdays[0]] 

428 

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

430 

431 

432class _TimedeltaFrequencyInferer(_FrequencyInferer): 

433 def _infer_daily_rule(self): 

434 if self.is_unique: 

435 return self._get_daily_rule() 

436 

437 

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

439 return us % mult == 0 

440 

441 

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

443 if count != 1: 

444 assert count == int(count) 

445 count = int(count) 

446 return f"{count}{base}" 

447 else: 

448 return base 

449 

450 

451# ---------------------------------------------------------------------- 

452# Frequency comparison 

453 

454 

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

456 """ 

457 Returns True if downsampling is possible between source and target 

458 frequencies 

459 

460 Parameters 

461 ---------- 

462 source : str or DateOffset 

463 Frequency converting from 

464 target : str or DateOffset 

465 Frequency converting to 

466 

467 Returns 

468 ------- 

469 bool 

470 """ 

471 if target is None or source is None: 

472 return False 

473 source = _maybe_coerce_freq(source) 

474 target = _maybe_coerce_freq(target) 

475 

476 if _is_annual(target): 

477 if _is_quarterly(source): 

478 return _quarter_months_conform( 

479 get_rule_month(source), get_rule_month(target) 

480 ) 

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

482 elif _is_quarterly(target): 

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

484 elif _is_monthly(target): 

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

486 elif _is_weekly(target): 

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

488 elif target == "B": 

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

490 elif target == "C": 

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

492 elif target == "D": 

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

494 elif target == "h": 

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

496 elif target == "min": 

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

498 elif target == "s": 

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

500 elif target == "ms": 

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

502 elif target == "us": 

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

504 elif target == "ns": 

505 return source in {"ns"} 

506 else: 

507 return False 

508 

509 

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

511 """ 

512 Returns True if upsampling is possible between source and target 

513 frequencies 

514 

515 Parameters 

516 ---------- 

517 source : str or DateOffset 

518 Frequency converting from 

519 target : str or DateOffset 

520 Frequency converting to 

521 

522 Returns 

523 ------- 

524 bool 

525 """ 

526 if target is None or source is None: 

527 return False 

528 source = _maybe_coerce_freq(source) 

529 target = _maybe_coerce_freq(target) 

530 

531 if _is_annual(source): 

532 if _is_annual(target): 

533 return get_rule_month(source) == get_rule_month(target) 

534 

535 if _is_quarterly(target): 

536 smonth = get_rule_month(source) 

537 tmonth = get_rule_month(target) 

538 return _quarter_months_conform(smonth, tmonth) 

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

540 elif _is_quarterly(source): 

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

542 elif _is_monthly(source): 

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

544 elif _is_weekly(source): 

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

546 elif source == "B": 

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

548 elif source == "C": 

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

550 elif source == "D": 

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

552 elif source == "h": 

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

554 elif source == "min": 

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

556 elif source == "s": 

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

558 elif source == "ms": 

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

560 elif source == "us": 

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

562 elif source == "ns": 

563 return target in {"ns"} 

564 else: 

565 return False 

566 

567 

568def _maybe_coerce_freq(code) -> str: 

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

570 and uppercase it 

571 

572 Parameters 

573 ---------- 

574 source : str or DateOffset 

575 Frequency converting from 

576 

577 Returns 

578 ------- 

579 str 

580 """ 

581 assert code is not None 

582 if isinstance(code, DateOffset): 

583 code = PeriodDtype(to_offset(code.name))._freqstr 

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

585 return code 

586 else: 

587 return code.upper() 

588 

589 

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

591 snum = MONTH_NUMBERS[source] 

592 tnum = MONTH_NUMBERS[target] 

593 return snum % 3 == tnum % 3 

594 

595 

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

597 rule = rule.upper() 

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

599 

600 

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

602 rule = rule.upper() 

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

604 

605 

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

607 rule = rule.upper() 

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

609 

610 

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

612 rule = rule.upper() 

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

614 

615 

616__all__ = [ 

617 "Day", 

618 "get_period_alias", 

619 "infer_freq", 

620 "is_subperiod", 

621 "is_superperiod", 

622 "to_offset", 

623]