Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pandas/core/indexes/period.py: 39%

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

194 statements  

1from __future__ import annotations 

2 

3from datetime import ( 

4 datetime, 

5 timedelta, 

6) 

7from typing import ( 

8 TYPE_CHECKING, 

9 Self, 

10) 

11 

12import numpy as np 

13 

14from pandas._libs import index as libindex 

15from pandas._libs.tslibs import ( 

16 BaseOffset, 

17 Day, 

18 NaT, 

19 Period, 

20 Resolution, 

21 Tick, 

22) 

23from pandas._libs.tslibs.dtypes import OFFSET_TO_PERIOD_FREQSTR 

24from pandas.util._decorators import ( 

25 cache_readonly, 

26 doc, 

27 set_module, 

28) 

29 

30from pandas.core.dtypes.common import is_integer 

31from pandas.core.dtypes.dtypes import PeriodDtype 

32from pandas.core.dtypes.generic import ABCSeries 

33from pandas.core.dtypes.missing import is_valid_na_for_dtype 

34 

35from pandas.core.arrays.period import ( 

36 PeriodArray, 

37 period_array, 

38 raise_on_incompatible, 

39 validate_dtype_freq, 

40) 

41import pandas.core.common as com 

42import pandas.core.indexes.base as ibase 

43from pandas.core.indexes.base import maybe_extract_name 

44from pandas.core.indexes.datetimelike import DatetimeIndexOpsMixin 

45from pandas.core.indexes.datetimes import ( 

46 DatetimeIndex, 

47 Index, 

48) 

49from pandas.core.indexes.extension import inherit_names 

50 

51if TYPE_CHECKING: 

52 from collections.abc import Hashable 

53 

54 from pandas._typing import ( 

55 Dtype, 

56 DtypeObj, 

57 npt, 

58 ) 

59 

60 

61_index_doc_kwargs = dict(ibase._index_doc_kwargs) 

62_index_doc_kwargs.update({"target_klass": "PeriodIndex or list of Periods"}) 

63_shared_doc_kwargs = { 

64 "klass": "PeriodArray", 

65} 

66 

67# --- Period index sketch 

68 

69 

70def _new_PeriodIndex(cls, **d): 

71 # GH13277 for unpickling 

72 values = d.pop("data") 

73 if values.dtype == "int64": 

74 freq = d.pop("freq", None) 

75 dtype = PeriodDtype(freq) 

76 values = PeriodArray(values, dtype=dtype) 

77 return cls._simple_new(values, **d) 

78 else: 

79 return cls(values, **d) 

80 

81 

82@inherit_names( 

83 ["strftime", "start_time", "end_time", *PeriodArray._field_ops], 

84 PeriodArray, 

85 wrap=True, 

86) 

87@inherit_names(["is_leap_year"], PeriodArray) 

88@set_module("pandas") 

89class PeriodIndex(DatetimeIndexOpsMixin): 

90 """ 

91 Immutable ndarray holding ordinal values indicating regular periods in time. 

92 

93 Index keys are boxed to Period objects which carries the metadata (eg, 

94 frequency information). 

95 

96 Parameters 

97 ---------- 

98 data : array-like (1d int np.ndarray or PeriodArray), optional 

99 Optional period-like data to construct index with. 

100 freq : str or period object, optional 

101 One of pandas period strings or corresponding objects. 

102 dtype : str or PeriodDtype, default None 

103 A dtype from which to extract a freq. 

104 copy : bool, default None 

105 Whether to copy input data, only relevant for array, Series, and Index 

106 inputs (for other input, e.g. a list, a new array is created anyway). 

107 Defaults to True for array input and False for Index/Series. 

108 Set to False to avoid copying array input at your own risk (if you 

109 know the input data won't be modified elsewhere). 

110 Set to True to force copying Series/Index input up front. 

111 name : str, default None 

112 Name of the resulting PeriodIndex. 

113 

114 Attributes 

115 ---------- 

116 day 

117 dayofweek 

118 day_of_week 

119 dayofyear 

120 day_of_year 

121 days_in_month 

122 daysinmonth 

123 end_time 

124 freq 

125 freqstr 

126 hour 

127 is_leap_year 

128 minute 

129 month 

130 quarter 

131 qyear 

132 second 

133 start_time 

134 week 

135 weekday 

136 weekofyear 

137 year 

138 

139 Methods 

140 ------- 

141 asfreq 

142 strftime 

143 to_timestamp 

144 from_fields 

145 from_ordinals 

146 

147 Raises 

148 ------ 

149 ValueError 

150 Passing the parameter data as a list without specifying either freq or 

151 dtype will raise a ValueError: "freq not specified and cannot be inferred" 

152 

153 See Also 

154 -------- 

155 Index : The base pandas Index type. 

156 Period : Represents a period of time. 

157 DatetimeIndex : Index with datetime64 data. 

158 TimedeltaIndex : Index of timedelta64 data. 

159 period_range : Create a fixed-frequency PeriodIndex. 

160 

161 Examples 

162 -------- 

163 >>> idx = pd.PeriodIndex(data=["2000Q1", "2002Q3"], freq="Q") 

164 >>> idx 

165 PeriodIndex(['2000Q1', '2002Q3'], dtype='period[Q-DEC]') 

166 """ 

167 

168 _typ = "periodindex" 

169 

170 _data: PeriodArray 

171 freq: BaseOffset 

172 dtype: PeriodDtype 

173 

174 _data_cls = PeriodArray 

175 _supports_partial_string_indexing = True 

176 

177 @property 

178 def _engine_type(self) -> type[libindex.PeriodEngine]: 

179 return libindex.PeriodEngine 

180 

181 @cache_readonly 

182 def _resolution_obj(self) -> Resolution: 

183 # for compat with DatetimeIndex 

184 return self.dtype._resolution_obj 

185 

186 # -------------------------------------------------------------------- 

187 # methods that dispatch to array and wrap result in Index 

188 # These are defined here instead of via inherit_names for mypy 

189 

190 @doc( 

191 PeriodArray.asfreq, 

192 other="arrays.PeriodArray", 

193 other_name="PeriodArray", 

194 **_shared_doc_kwargs, 

195 ) 

196 def asfreq(self, freq=None, how: str = "E") -> Self: 

197 arr = self._data.asfreq(freq, how) 

198 return type(self)._simple_new(arr, name=self.name) 

199 

200 @doc(PeriodArray.to_timestamp) 

201 def to_timestamp(self, freq=None, how: str = "start") -> DatetimeIndex: 

202 arr = self._data.to_timestamp(freq, how) 

203 return DatetimeIndex._simple_new(arr, name=self.name) 

204 

205 @property 

206 @doc(PeriodArray.hour.fget) 

207 def hour(self) -> Index: 

208 return Index(self._data.hour, name=self.name, copy=False) 

209 

210 @property 

211 @doc(PeriodArray.minute.fget) 

212 def minute(self) -> Index: 

213 return Index(self._data.minute, name=self.name, copy=False) 

214 

215 @property 

216 @doc(PeriodArray.second.fget) 

217 def second(self) -> Index: 

218 return Index(self._data.second, name=self.name, copy=False) 

219 

220 # ------------------------------------------------------------------------ 

221 # Index Constructors 

222 

223 def __new__( 

224 cls, 

225 data=None, 

226 freq=None, 

227 dtype: Dtype | None = None, 

228 copy: bool | None = None, 

229 name: Hashable | None = None, 

230 ) -> Self: 

231 refs = None 

232 if not copy and isinstance(data, (Index, ABCSeries)): 

233 refs = data._references 

234 

235 name = maybe_extract_name(name, data, cls) 

236 

237 freq = validate_dtype_freq(dtype, freq) 

238 

239 # GH#63388 

240 data, copy = cls._maybe_copy_array_input(data, copy, dtype) 

241 

242 # PeriodIndex allow PeriodIndex(period_index, freq=different) 

243 # Let's not encourage that kind of behavior in PeriodArray. 

244 

245 if freq and isinstance(data, cls) and data.freq != freq: 

246 # TODO: We can do some of these with no-copy / coercion? 

247 # e.g. D -> 2D seems to be OK 

248 data = data.asfreq(freq) 

249 

250 # don't pass copy here, since we copy later. 

251 data = period_array(data=data, freq=freq) 

252 

253 if copy: 

254 data = data.copy() 

255 

256 return cls._simple_new(data, name=name, refs=refs) 

257 

258 @classmethod 

259 def from_fields( 

260 cls, 

261 *, 

262 year=None, 

263 quarter=None, 

264 month=None, 

265 day=None, 

266 hour=None, 

267 minute=None, 

268 second=None, 

269 freq=None, 

270 ) -> Self: 

271 """ 

272 Construct a PeriodIndex from fields (year, month, day, etc.). 

273 

274 Parameters 

275 ---------- 

276 year : int, array, or Series, default None 

277 Year for the PeriodIndex. 

278 quarter : int, array, or Series, default None 

279 Quarter for the PeriodIndex. 

280 month : int, array, or Series, default None 

281 Month for the PeriodIndex. 

282 day : int, array, or Series, default None 

283 Day for the PeriodIndex. 

284 hour : int, array, or Series, default None 

285 Hour for the PeriodIndex. 

286 minute : int, array, or Series, default None 

287 Minute for the PeriodIndex. 

288 second : int, array, or Series, default None 

289 Second for the PeriodIndex. 

290 freq : str or period object, optional 

291 One of pandas period strings or corresponding objects. 

292 

293 Returns 

294 ------- 

295 PeriodIndex 

296 

297 See Also 

298 -------- 

299 PeriodIndex.from_ordinals : Construct a PeriodIndex from ordinals. 

300 PeriodIndex.to_timestamp : Cast to DatetimeArray/Index. 

301 

302 Examples 

303 -------- 

304 >>> idx = pd.PeriodIndex.from_fields(year=[2000, 2002], quarter=[1, 3]) 

305 >>> idx 

306 PeriodIndex(['2000Q1', '2002Q3'], dtype='period[Q-DEC]') 

307 """ 

308 fields = { 

309 "year": year, 

310 "quarter": quarter, 

311 "month": month, 

312 "day": day, 

313 "hour": hour, 

314 "minute": minute, 

315 "second": second, 

316 } 

317 fields = {key: value for key, value in fields.items() if value is not None} 

318 arr = PeriodArray._from_fields(fields=fields, freq=freq) 

319 return cls._simple_new(arr) 

320 

321 @classmethod 

322 def from_ordinals(cls, ordinals, *, freq, name=None) -> Self: 

323 """ 

324 Construct a PeriodIndex from ordinals. 

325 

326 Parameters 

327 ---------- 

328 ordinals : array-like of int 

329 The period offsets from the proleptic Gregorian epoch. 

330 freq : str or period object 

331 One of pandas period strings or corresponding objects. 

332 name : str, default None 

333 Name of the resulting PeriodIndex. 

334 

335 Returns 

336 ------- 

337 PeriodIndex 

338 

339 See Also 

340 -------- 

341 PeriodIndex.from_fields : Construct a PeriodIndex from fields 

342 (year, month, day, etc.). 

343 PeriodIndex.to_timestamp : Cast to DatetimeArray/Index. 

344 

345 Examples 

346 -------- 

347 >>> idx = pd.PeriodIndex.from_ordinals([-1, 0, 1], freq="Q") 

348 >>> idx 

349 PeriodIndex(['1969Q4', '1970Q1', '1970Q2'], dtype='period[Q-DEC]') 

350 """ 

351 ordinals = np.asarray(ordinals, dtype=np.int64) 

352 dtype = PeriodDtype(freq) 

353 data = PeriodArray._simple_new(ordinals, dtype=dtype) 

354 return cls._simple_new(data, name=name) 

355 

356 # ------------------------------------------------------------------------ 

357 # Data 

358 

359 @property 

360 def values(self) -> npt.NDArray[np.object_]: 

361 return np.asarray(self, dtype=object) 

362 

363 def _maybe_convert_timedelta(self, other) -> int | npt.NDArray[np.int64]: 

364 """ 

365 Convert timedelta-like input to an integer multiple of self.freq 

366 

367 Parameters 

368 ---------- 

369 other : timedelta, np.timedelta64, DateOffset, int, np.ndarray 

370 

371 Returns 

372 ------- 

373 converted : int, np.ndarray[int64] 

374 

375 Raises 

376 ------ 

377 IncompatibleFrequency : if the input cannot be written as a multiple 

378 of self.freq. Note IncompatibleFrequency subclasses ValueError. 

379 """ 

380 if isinstance(other, (timedelta, np.timedelta64, Tick, np.ndarray)): 

381 if isinstance(self.freq, (Tick, Day)): 

382 # _check_timedeltalike_freq_compat will raise if incompatible 

383 delta = self._data._check_timedeltalike_freq_compat(other) 

384 return delta 

385 elif isinstance(other, BaseOffset): 

386 if other.base == self.freq.base: 

387 return other.n 

388 

389 raise raise_on_incompatible(self, other) 

390 elif is_integer(other): 

391 assert isinstance(other, int) 

392 return other 

393 

394 # raise when input doesn't have freq 

395 raise raise_on_incompatible(self, None) 

396 

397 def _is_comparable_dtype(self, dtype: DtypeObj) -> bool: 

398 """ 

399 Can we compare values of the given dtype to our own? 

400 """ 

401 return self.dtype == dtype 

402 

403 # ------------------------------------------------------------------------ 

404 # Index Methods 

405 

406 def asof_locs(self, where: Index, mask: npt.NDArray[np.bool_]) -> np.ndarray: 

407 """ 

408 where : array of timestamps 

409 mask : np.ndarray[bool] 

410 Array of booleans where data is not NA. 

411 """ 

412 if isinstance(where, DatetimeIndex): 

413 where = PeriodIndex(where._values, freq=self.freq, copy=False) 

414 elif not isinstance(where, PeriodIndex): 

415 raise TypeError("asof_locs `where` must be DatetimeIndex or PeriodIndex") 

416 

417 return super().asof_locs(where, mask) 

418 

419 @property 

420 def is_full(self) -> bool: 

421 """ 

422 Returns True if this PeriodIndex is range-like in that all Periods 

423 between start and end are present, in order. 

424 """ 

425 if len(self) == 0: 

426 return True 

427 if not self.is_monotonic_increasing: 

428 raise ValueError("Index is not monotonic") 

429 values = self.asi8 

430 return bool(((values[1:] - values[:-1]) < 2).all()) 

431 

432 @property 

433 def inferred_type(self) -> str: 

434 # b/c data is represented as ints make sure we can't have ambiguous 

435 # indexing 

436 return "period" 

437 

438 # ------------------------------------------------------------------------ 

439 # Indexing Methods 

440 

441 def _convert_tolerance(self, tolerance, target): 

442 # Returned tolerance must be in dtype/units so that 

443 # `|self._get_engine_target() - target._engine_target()| <= tolerance` 

444 # is meaningful. Since PeriodIndex returns int64 for engine_target, 

445 # we may need to convert timedelta64 tolerance to int64. 

446 tolerance = super()._convert_tolerance(tolerance, target) 

447 

448 if self.dtype == target.dtype: 

449 # convert tolerance to i8 

450 tolerance = self._maybe_convert_timedelta(tolerance) 

451 

452 return tolerance 

453 

454 def get_loc(self, key): 

455 """ 

456 Get integer location for requested label. 

457 

458 Parameters 

459 ---------- 

460 key : Period, NaT, str, or datetime 

461 String or datetime key must be parsable as Period. 

462 

463 Returns 

464 ------- 

465 loc : int or ndarray[int64] 

466 

467 Raises 

468 ------ 

469 KeyError 

470 Key is not present in the index. 

471 TypeError 

472 If key is listlike or otherwise not hashable. 

473 """ 

474 orig_key = key 

475 

476 self._check_indexing_error(key) 

477 

478 if is_valid_na_for_dtype(key, self.dtype): 

479 key = NaT 

480 

481 elif isinstance(key, str): 

482 try: 

483 parsed, reso = self._parse_with_reso(key) 

484 except ValueError as err: 

485 # A string with invalid format 

486 raise KeyError(f"Cannot interpret '{key}' as period") from err 

487 

488 if self._can_partial_date_slice(reso): 

489 try: 

490 return self._partial_date_slice(reso, parsed) 

491 except KeyError as err: 

492 raise KeyError(key) from err 

493 

494 if reso == self._resolution_obj: 

495 # the reso < self._resolution_obj case goes 

496 # through _get_string_slice 

497 key = self._cast_partial_indexing_scalar(parsed) 

498 else: 

499 raise KeyError(key) 

500 

501 elif isinstance(key, Period): 

502 self._disallow_mismatched_indexing(key) 

503 

504 elif isinstance(key, datetime): 

505 key = self._cast_partial_indexing_scalar(key) 

506 

507 else: 

508 # in particular integer, which Period constructor would cast to string 

509 raise KeyError(key) 

510 

511 try: 

512 return Index.get_loc(self, key) 

513 except KeyError as err: 

514 raise KeyError(orig_key) from err 

515 

516 def _disallow_mismatched_indexing(self, key: Period) -> None: 

517 if key._dtype != self.dtype: 

518 raise KeyError(key) 

519 

520 def _cast_partial_indexing_scalar(self, label: datetime) -> Period: 

521 try: 

522 period = Period(label, freq=self.freq) 

523 except ValueError as err: 

524 # we cannot construct the Period 

525 raise KeyError(label) from err 

526 return period 

527 

528 @doc(DatetimeIndexOpsMixin._maybe_cast_slice_bound) 

529 def _maybe_cast_slice_bound(self, label, side: str): 

530 if isinstance(label, datetime): 

531 label = self._cast_partial_indexing_scalar(label) 

532 

533 return super()._maybe_cast_slice_bound(label, side) 

534 

535 def _parsed_string_to_bounds(self, reso: Resolution, parsed: datetime): 

536 freq = OFFSET_TO_PERIOD_FREQSTR.get(reso.attr_abbrev, reso.attr_abbrev) 

537 iv = Period(parsed, freq=freq) 

538 return (iv.asfreq(self.freq, how="start"), iv.asfreq(self.freq, how="end")) 

539 

540 @doc(DatetimeIndexOpsMixin.shift) 

541 def shift(self, periods: int = 1, freq=None) -> Self: 

542 if freq is not None: 

543 raise TypeError( 

544 f"`freq` argument is not supported for {type(self).__name__}.shift" 

545 ) 

546 return self + periods 

547 

548 

549@set_module("pandas") 

550def period_range( 

551 start=None, 

552 end=None, 

553 periods: int | None = None, 

554 freq=None, 

555 name: Hashable | None = None, 

556) -> PeriodIndex: 

557 """ 

558 Return a fixed frequency PeriodIndex. 

559 

560 The day (calendar) is the default frequency. 

561 

562 Parameters 

563 ---------- 

564 start : str, datetime, date, pandas.Timestamp, or period-like, default None 

565 Left bound for generating periods. 

566 end : str, datetime, date, pandas.Timestamp, or period-like, default None 

567 Right bound for generating periods. 

568 periods : int, default None 

569 Number of periods to generate. 

570 freq : str or DateOffset, optional 

571 Frequency alias. By default the freq is taken from `start` or `end` 

572 if those are Period objects. Otherwise, the default is ``"D"`` for 

573 daily frequency. 

574 name : str, default None 

575 Name of the resulting PeriodIndex. 

576 

577 Returns 

578 ------- 

579 PeriodIndex 

580 A PeriodIndex of fixed frequency periods. 

581 

582 See Also 

583 -------- 

584 date_range : Returns a fixed frequency DatetimeIndex. 

585 Period : Represents a period of time. 

586 PeriodIndex : Immutable ndarray holding ordinal values indicating regular periods 

587 in time. 

588 

589 Notes 

590 ----- 

591 Of the three parameters: ``start``, ``end``, and ``periods``, exactly two 

592 must be specified. 

593 

594 To learn more about the frequency strings, please see 

595 :ref:`this link<timeseries.offset_aliases>`. 

596 

597 Examples 

598 -------- 

599 >>> pd.period_range(start="2017-01-01", end="2018-01-01", freq="M") 

600 PeriodIndex(['2017-01', '2017-02', '2017-03', '2017-04', '2017-05', '2017-06', 

601 '2017-07', '2017-08', '2017-09', '2017-10', '2017-11', '2017-12', 

602 '2018-01'], 

603 dtype='period[M]') 

604 

605 If ``start`` or ``end`` are ``Period`` objects, they will be used as anchor 

606 endpoints for a ``PeriodIndex`` with frequency matching that of the 

607 ``period_range`` constructor. 

608 

609 >>> pd.period_range( 

610 ... start=pd.Period("2017Q1", freq="Q"), 

611 ... end=pd.Period("2017Q2", freq="Q"), 

612 ... freq="M", 

613 ... ) 

614 PeriodIndex(['2017-03', '2017-04', '2017-05', '2017-06'], 

615 dtype='period[M]') 

616 """ 

617 if com.count_not_none(start, end, periods) != 2: 

618 raise ValueError( 

619 "Of the three parameters: start, end, and periods, " 

620 "exactly two must be specified" 

621 ) 

622 if freq is None and (not isinstance(start, Period) and not isinstance(end, Period)): 

623 freq = "D" 

624 

625 data, freq = PeriodArray._generate_range(start, end, periods, freq) 

626 dtype = PeriodDtype(freq) 

627 data = PeriodArray(data, dtype=dtype) 

628 return PeriodIndex(data, name=name, copy=False)