1from __future__ import annotations
2
3from datetime import (
4 datetime,
5 timedelta,
6)
7from typing import (
8 TYPE_CHECKING,
9 Self,
10)
11import warnings
12
13import numpy as np
14
15from pandas._libs import index as libindex
16from pandas._libs.tslibs import (
17 BaseOffset,
18 Day,
19 NaT,
20 Period,
21 Resolution,
22 Tick,
23 to_offset,
24)
25from pandas._libs.tslibs.dtypes import OFFSET_TO_PERIOD_FREQSTR
26from pandas.errors import Pandas4Warning
27from pandas.util._decorators import (
28 set_module,
29)
30from pandas.util._exceptions import find_stack_level
31
32from pandas.core.dtypes.common import (
33 is_integer,
34 pandas_dtype,
35)
36from pandas.core.dtypes.dtypes import PeriodDtype
37from pandas.core.dtypes.generic import ABCSeries
38from pandas.core.dtypes.missing import is_valid_na_for_dtype
39
40from pandas.core.arrays.period import (
41 PeriodArray,
42 period_array,
43 raise_on_incompatible,
44 validate_dtype_freq,
45)
46import pandas.core.common as com
47from pandas.core.indexes.base import maybe_extract_name
48from pandas.core.indexes.datetimelike import DatetimeIndexOpsMixin
49from pandas.core.indexes.datetimes import (
50 DatetimeIndex,
51 Index,
52)
53
54if TYPE_CHECKING:
55 from collections.abc import Hashable
56
57 from pandas._typing import (
58 Dtype,
59 DtypeObj,
60 npt,
61 )
62
63
64def _new_PeriodIndex(cls, **d):
65 # GH13277 for unpickling
66 values = d.pop("data")
67 if values.dtype == "int64":
68 freq = d.pop("freq", None)
69 dtype = PeriodDtype(freq)
70 values = PeriodArray(values, dtype=dtype)
71 return cls._simple_new(values, **d)
72 else:
73 return cls(values, **d)
74
75
76@set_module("pandas")
77class PeriodIndex(DatetimeIndexOpsMixin):
78 """
79 Immutable ndarray holding ordinal values indicating regular periods in time.
80
81 Index keys are boxed to Period objects which carries the metadata (eg,
82 frequency information).
83
84 Parameters
85 ----------
86 data : array-like (1d int np.ndarray or PeriodArray), optional
87 Optional period-like data to construct index with.
88 freq : str or period object, optional
89 One of pandas period strings or corresponding objects.
90 dtype : str or PeriodDtype, default None
91 A dtype from which to extract a freq.
92 copy : bool, default None
93 Whether to copy input data, only relevant for array, Series, and Index
94 inputs (for other input, e.g. a list, a new array is created anyway).
95 Defaults to True for array input and False for Index/Series.
96 Set to False to avoid copying array input at your own risk (if you
97 know the input data won't be modified elsewhere).
98 Set to True to force copying Series/Index input up front.
99 name : str, default None
100 Name of the resulting PeriodIndex.
101
102 Attributes
103 ----------
104 day
105 dayofweek
106 day_of_week
107 dayofyear
108 day_of_year
109 days_in_month
110 daysinmonth
111 end_time
112 freq
113 freqstr
114 hour
115 is_leap_year
116 minute
117 month
118 quarter
119 qyear
120 second
121 start_time
122 week
123 weekday
124 weekofyear
125 year
126
127 Methods
128 -------
129 asfreq
130 strftime
131 to_timestamp
132 from_fields
133 from_ordinals
134
135 Raises
136 ------
137 ValueError
138 Passing the parameter data as a list without specifying either freq or
139 dtype will raise a ValueError: "freq not specified and cannot be inferred"
140
141 See Also
142 --------
143 Index : The base pandas Index type.
144 Period : Represents a period of time.
145 DatetimeIndex : Index with datetime64 data.
146 TimedeltaIndex : Index of timedelta64 data.
147 period_range : Create a fixed-frequency PeriodIndex.
148
149 Examples
150 --------
151 >>> idx = pd.PeriodIndex(data=["2000Q1", "2002Q3"], freq="Q")
152 >>> idx
153 PeriodIndex(['2000Q1', '2002Q3'], dtype='period[Q-DEC]')
154 """
155
156 _typ = "periodindex"
157
158 _data: PeriodArray
159 freq: BaseOffset
160 dtype: PeriodDtype
161
162 _data_cls = PeriodArray
163 _supports_partial_string_indexing = True
164
165 _warn_quarter: bool = False
166
167 @property
168 def _engine_type(self) -> type[libindex.PeriodEngine]:
169 return libindex.PeriodEngine
170
171 # --------------------------------------------------------------------
172 # methods that dispatch to array and wrap result in Index
173
174 def asfreq(self, freq=None, how: str = "E") -> Self:
175 """
176 Convert the PeriodIndex to the specified frequency `freq`.
177
178 Equivalent to applying :meth:`pandas.Period.asfreq` with the given arguments
179 to each :class:`~pandas.Period` in this PeriodIndex.
180
181 Parameters
182 ----------
183 freq : str
184 A frequency.
185 how : {'end', 'start', 'e', 's'}, default 'end'
186 Whether the elements should be aligned to the end or start of
187 each period, e.g. January 31st vs. January 1st. Case-insensitive.
188
189 Returns
190 -------
191 PeriodIndex
192 The transformed PeriodIndex with the new frequency.
193
194 See Also
195 --------
196 arrays.PeriodArray.asfreq: Convert each Period in a PeriodArray to
197 the given frequency.
198 Period.asfreq : Convert a :class:`~pandas.Period` object to the given frequency.
199
200 Examples
201 --------
202 >>> pidx = pd.period_range("2010-01-01", "2015-01-01", freq="Y")
203 >>> pidx
204 PeriodIndex(['2010', '2011', '2012', '2013', '2014', '2015'],
205 dtype='period[Y-DEC]')
206
207 >>> pidx.asfreq("M")
208 PeriodIndex(['2010-12', '2011-12', '2012-12', '2013-12', '2014-12',
209 '2015-12'], dtype='period[M]')
210
211 >>> pidx.asfreq("M", how="S")
212 PeriodIndex(['2010-01', '2011-01', '2012-01', '2013-01', '2014-01',
213 '2015-01'], dtype='period[M]')
214 """
215 arr = self._data.asfreq(freq, how)
216 return type(self)._simple_new(arr, name=self.name)
217
218 def to_timestamp(self, freq=None, how: str = "start") -> DatetimeIndex:
219 """
220 Cast to DatetimeIndex.
221
222 If possible, gives microsecond-unit DatetimeIndex. Otherwise
223 gives nanosecond unit.
224
225 Parameters
226 ----------
227 freq : str or DateOffset, optional
228 Target frequency. The default is 'D' for week or longer,
229 's' otherwise.
230 how : {'start', 'end', 's', 'e'}, default 'start'
231 Whether to use the start or end of the time period being converted.
232 Case-insensitive.
233
234 Returns
235 -------
236 DatetimeIndex
237 Timestamp representation of given Period-like object.
238
239 See Also
240 --------
241 PeriodIndex.day : The days of the period.
242 PeriodIndex.from_fields : Construct a PeriodIndex from fields
243 (year, month, day, etc.).
244 PeriodIndex.from_ordinals : Construct a PeriodIndex from ordinals.
245 PeriodIndex.hour : The hour of the period.
246 PeriodIndex.minute : The minute of the period.
247 PeriodIndex.month : The month as January=1, December=12.
248 PeriodIndex.second : The second of the period.
249 PeriodIndex.year : The year of the period.
250
251 Examples
252 --------
253 >>> idx = pd.PeriodIndex(["2023-01", "2023-02", "2023-03"], freq="M")
254 >>> idx.to_timestamp()
255 DatetimeIndex(['2023-01-01', '2023-02-01', '2023-03-01'],
256 dtype='datetime64[us]', freq='MS')
257
258 The frequency will not be inferred if the index contains less than
259 three elements, or if the values of index are not strictly monotonic:
260
261 >>> idx = pd.PeriodIndex(["2023-01", "2023-02"], freq="M")
262 >>> idx.to_timestamp()
263 DatetimeIndex(['2023-01-01', '2023-02-01'], dtype='datetime64[us]', freq=None)
264
265 >>> idx = pd.PeriodIndex(
266 ... ["2023-01", "2023-02", "2023-02", "2023-03"], freq="2M"
267 ... )
268 >>> idx.to_timestamp()
269 DatetimeIndex(['2023-01-01', '2023-02-01', '2023-02-01', '2023-03-01'],
270 dtype='datetime64[us]', freq=None)
271 """
272 parr = self._data
273 arr = parr.to_timestamp(freq, how)
274 result = DatetimeIndex._simple_new(arr, name=self.name)
275 result._freq = parr._to_timestamp_freq(arr, target_freq=freq, how=how)
276 return result
277
278 def strftime(self, date_format: str) -> Index:
279 """
280 Convert to Index using specified date_format.
281
282 Return an Index of formatted strings specified by date_format, which
283 supports the same string format as the python standard library. Details
284 of the string format can be found in `python string format
285 doc <https://docs.python.org/3/library/datetime.html#strftime-and-strptime-behavior>`__.
286 :class:`Period` objects additionally support several directives not
287 covered there, detailed in :meth:`Period.strftime`.
288
289 Parameters
290 ----------
291 date_format : str
292 Date format string (e.g. "%Y-%m-%d").
293
294 Returns
295 -------
296 Index
297 Index of formatted strings.
298
299 See Also
300 --------
301 to_datetime : Convert the given argument to datetime.
302 Period.strftime : Format a single Period.
303
304 Examples
305 --------
306 >>> idx = pd.PeriodIndex(["2023-01", "2023-02", "2023-03"], freq="M")
307 >>> idx.strftime("%B %d, %Y")
308 Index(['January 31, 2023', 'February 28, 2023', 'March 31, 2023'], dtype='str')
309 """
310 arr = self._data.strftime(date_format)
311 return Index(arr, name=self.name, dtype=arr.dtype, copy=False)
312
313 def _wrap_field(self, name: str) -> Index:
314 result = getattr(self._data, name)
315 return Index(result, name=self.name, dtype=result.dtype, copy=False)
316
317 @property
318 def start_time(self) -> DatetimeIndex:
319 """
320 Get the Timestamp for the start of the period.
321
322 Returns a DatetimeIndex with the exact start Timestamp of each
323 period in the index.
324
325 Returns
326 -------
327 DatetimeIndex
328
329 See Also
330 --------
331 PeriodIndex.end_time : Return the end Timestamp.
332 PeriodIndex.to_timestamp : Cast to DatetimeIndex.
333 Period.start_time : Return the start Timestamp for a single Period.
334
335 Examples
336 --------
337 >>> idx = pd.PeriodIndex(["2023-01", "2023-02"], freq="M")
338 >>> idx.start_time
339 DatetimeIndex(['2023-01-01', '2023-02-01'], dtype='datetime64[us]', freq=None)
340 """
341 return DatetimeIndex(self._data.start_time, name=self.name, copy=False)
342
343 @property
344 def end_time(self) -> DatetimeIndex:
345 """
346 Get the Timestamp for the end of the period.
347
348 Returns a DatetimeIndex with the last possible moment within each
349 period in the index (e.g. 23:59:59.999999 for a daily period).
350
351 Returns
352 -------
353 DatetimeIndex
354
355 See Also
356 --------
357 PeriodIndex.start_time : Return the start Timestamp.
358 PeriodIndex.to_timestamp : Cast to DatetimeIndex.
359 Period.end_time : Return the end Timestamp for a single Period.
360
361 Examples
362 --------
363 >>> idx = pd.PeriodIndex(["2023-01", "2023-02"], freq="M")
364 >>> idx.end_time
365 DatetimeIndex(['2023-01-31 23:59:59.999999', '2023-02-28 23:59:59.999999'],
366 dtype='datetime64[us]', freq=None)
367 """
368 return DatetimeIndex(self._data.end_time, name=self.name, copy=False)
369
370 @property
371 def year(self) -> Index:
372 """
373 The year of the period.
374
375 Returns the year component for each period in the index.
376
377 See Also
378 --------
379 PeriodIndex.day_of_year : The ordinal day of the year.
380 PeriodIndex.is_leap_year : Logical indicating if the date belongs to a
381 leap year.
382 PeriodIndex.weekofyear : The week ordinal of the year.
383
384 Examples
385 --------
386 >>> idx = pd.PeriodIndex(["2023", "2024", "2025"], freq="Y")
387 >>> idx.year
388 Index([2023, 2024, 2025], dtype='int64')
389 """
390 return self._wrap_field("year")
391
392 @property
393 def month(self) -> Index:
394 """
395 The month as January=1, December=12.
396
397 Returns the month component for each period in the index as an
398 integer, where January is 1 and December is 12.
399
400 See Also
401 --------
402 PeriodIndex.days_in_month : The number of days in the month.
403
404 Examples
405 --------
406 >>> idx = pd.PeriodIndex(["2023-01", "2023-02", "2023-03"], freq="M")
407 >>> idx.month
408 Index([1, 2, 3], dtype='int64')
409 """
410 return self._wrap_field("month")
411
412 @property
413 def day(self) -> Index:
414 """
415 The days of the period.
416
417 Returns the day-of-month component for each period in the index.
418
419 See Also
420 --------
421 PeriodIndex.day_of_week : The day of the week with Monday=0, Sunday=6.
422 PeriodIndex.day_of_year : The ordinal day of the year.
423 PeriodIndex.days_in_month : The number of days in the month.
424
425 Examples
426 --------
427 >>> idx = pd.PeriodIndex(["2020-01-31", "2020-02-28"], freq="D")
428 >>> idx.day
429 Index([31, 28], dtype='int64')
430 """
431 return self._wrap_field("day")
432
433 @property
434 def hour(self) -> Index:
435 """
436 The hour of the period.
437
438 Returns the hour component for each period in the index.
439
440 See Also
441 --------
442 PeriodIndex.minute : The minute of the period.
443 PeriodIndex.second : The second of the period.
444 PeriodIndex.to_timestamp : Cast to DatetimeArray/Index.
445
446 Examples
447 --------
448 >>> idx = pd.PeriodIndex(["2023-01-01 10:00", "2023-01-01 11:00"], freq="h")
449 >>> idx.hour
450 Index([10, 11], dtype='int64')
451 """
452 return self._wrap_field("hour")
453
454 @property
455 def minute(self) -> Index:
456 """
457 The minute of the period.
458
459 Returns the minute component for each period in the index.
460
461 See Also
462 --------
463 PeriodIndex.hour : The hour of the period.
464 PeriodIndex.second : The second of the period.
465 PeriodIndex.to_timestamp : Cast to DatetimeArray/Index.
466
467 Examples
468 --------
469 >>> idx = pd.PeriodIndex(
470 ... ["2023-01-01 10:30:00", "2023-01-01 11:50:00"], freq="min"
471 ... )
472 >>> idx.minute
473 Index([30, 50], dtype='int64')
474 """
475 return self._wrap_field("minute")
476
477 @property
478 def second(self) -> Index:
479 """
480 The second of the period.
481
482 Returns the second component for each period in the index.
483
484 See Also
485 --------
486 PeriodIndex.hour : The hour of the period.
487 PeriodIndex.minute : The minute of the period.
488 PeriodIndex.to_timestamp : Cast to DatetimeArray/Index.
489
490 Examples
491 --------
492 >>> idx = pd.PeriodIndex(
493 ... ["2023-01-01 10:00:30", "2023-01-01 10:00:31"], freq="s"
494 ... )
495 >>> idx.second
496 Index([30, 31], dtype='int64')
497 """
498 return self._wrap_field("second")
499
500 @property
501 def weekofyear(self) -> Index:
502 """
503 The week ordinal of the year.
504
505 Returns the week number (1 through 53) for each period in the index.
506
507 See Also
508 --------
509 PeriodIndex.day_of_week : The day of the week with Monday=0, Sunday=6.
510 PeriodIndex.week : The week ordinal of the year.
511 PeriodIndex.year : The year of the period.
512
513 Examples
514 --------
515 >>> idx = pd.PeriodIndex(["2023-01", "2023-02", "2023-03"], freq="M")
516 >>> idx.week # It can be written `weekofyear`
517 Index([5, 9, 13], dtype='int64')
518 """
519 return self._wrap_field("weekofyear")
520
521 week = weekofyear
522
523 @property
524 def day_of_week(self) -> Index:
525 """
526 The day of the week with Monday=0, Sunday=6.
527
528 Returns the day-of-week component for each period, following the
529 Python convention where Monday is 0 and Sunday is 6.
530
531 See Also
532 --------
533 PeriodIndex.day : The days of the period.
534 PeriodIndex.day_of_year : The ordinal day of the year.
535 PeriodIndex.week : The week ordinal of the year.
536 PeriodIndex.weekofyear : The week ordinal of the year.
537
538 Examples
539 --------
540 >>> idx = pd.PeriodIndex(["2023-01-01", "2023-01-02", "2023-01-03"], freq="D")
541 >>> idx.day_of_week
542 Index([6, 0, 1], dtype='int64')
543 """
544 return self._wrap_field("day_of_week")
545
546 @property
547 def weekday(self) -> Index:
548 """
549 The day of the week with Monday=0, Sunday=6.
550
551 .. deprecated:: 3.1.0
552 Use :attr:`PeriodIndex.day_of_week` instead.
553 """
554 warnings.warn(
555 "PeriodIndex.weekday is deprecated and will be removed "
556 "in a future version. Use PeriodIndex.day_of_week instead.",
557 Pandas4Warning,
558 stacklevel=find_stack_level(),
559 )
560 return self._wrap_field("day_of_week")
561
562 @property
563 def dayofweek(self) -> Index:
564 """
565 The day of the week with Monday=0, Sunday=6.
566
567 .. deprecated:: 3.1.0
568 Use :attr:`PeriodIndex.day_of_week` instead.
569 """
570 warnings.warn(
571 "PeriodIndex.dayofweek is deprecated and will be removed "
572 "in a future version. Use PeriodIndex.day_of_week instead.",
573 Pandas4Warning,
574 stacklevel=find_stack_level(),
575 )
576 return self._wrap_field("day_of_week")
577
578 @property
579 def day_of_year(self) -> Index:
580 """
581 The ordinal day of the year.
582
583 Returns the day-of-year component for each period, ranging from
584 1 (January 1st) to 365 or 366 for leap years.
585
586 See Also
587 --------
588 PeriodIndex.day : The days of the period.
589 PeriodIndex.day_of_week : The day of the week with Monday=0, Sunday=6.
590 PeriodIndex.weekofyear : The week ordinal of the year.
591 PeriodIndex.year : The year of the period.
592
593 Examples
594 --------
595 >>> idx = pd.PeriodIndex(["2023-01-10", "2023-02-01", "2023-03-01"], freq="D")
596 >>> idx.day_of_year
597 Index([10, 32, 60], dtype='int64')
598
599 >>> idx = pd.PeriodIndex(["2023", "2024", "2025"], freq="Y")
600 >>> idx
601 PeriodIndex(['2023', '2024', '2025'], dtype='period[Y-DEC]')
602 >>> idx.day_of_year
603 Index([365, 366, 365], dtype='int64')
604 """
605 return self._wrap_field("day_of_year")
606
607 @property
608 def dayofyear(self) -> Index:
609 """
610 The ordinal day of the year.
611
612 .. deprecated:: 3.1.0
613 Use :attr:`PeriodIndex.day_of_year` instead.
614 """
615 warnings.warn(
616 "PeriodIndex.dayofyear is deprecated and will be removed in a "
617 "future version. Use PeriodIndex.day_of_year instead.",
618 Pandas4Warning,
619 stacklevel=find_stack_level(),
620 )
621 return self._wrap_field("day_of_year")
622
623 @property
624 def quarter(self) -> Index:
625 """
626 The quarter of the date.
627
628 Returns the quarter (1 through 4) for each period in the index.
629
630 See Also
631 --------
632 PeriodIndex.qyear : Fiscal year the Period lies in according to its
633 starting-quarter.
634
635 Examples
636 --------
637 >>> idx = pd.PeriodIndex(["2023-01", "2023-02", "2023-03"], freq="M")
638 >>> idx.quarter
639 Index([1, 1, 1], dtype='int64')
640 """
641 return self._wrap_field("quarter")
642
643 @property
644 def qyear(self) -> Index:
645 """
646 Fiscal year the Period lies in according to its starting-quarter.
647
648 The `year` and the `qyear` of the period will be the same if the fiscal
649 and calendar years are the same. When they are not, the fiscal year
650 can be different from the calendar year of the period.
651
652 Returns
653 -------
654 Index
655 The fiscal year of each period.
656
657 See Also
658 --------
659 PeriodIndex.quarter : The quarter of the date.
660 PeriodIndex.year : The year of the period.
661
662 Examples
663 --------
664 If the natural and fiscal year are the same, `qyear` and `year` will
665 be the same.
666
667 >>> idx = pd.PeriodIndex(["2018Q1"], freq="Q")
668 >>> idx.qyear
669 Index([2018], dtype='int64')
670 >>> idx.year
671 Index([2018], dtype='int64')
672
673 If the fiscal year starts in April (`Q-MAR`), the first quarter of
674 2018 will start in April 2017. `year` will then be 2017, but `qyear`
675 will be the fiscal year, 2018.
676
677 >>> idx = pd.PeriodIndex(["2018Q1"], freq="Q-MAR")
678 >>> idx.start_time
679 DatetimeIndex(['2017-04-01'], dtype='datetime64[us]', freq=None)
680 >>> idx.qyear
681 Index([2018], dtype='int64')
682 >>> idx.year
683 Index([2017], dtype='int64')
684 """
685 return self._wrap_field("qyear")
686
687 @property
688 def days_in_month(self) -> Index:
689 """
690 The number of days in the month.
691
692 Returns the total number of days in the month of each period,
693 accounting for leap years.
694
695 See Also
696 --------
697 PeriodIndex.day : The days of the period.
698 PeriodIndex.month : The month as January=1, December=12.
699
700 Examples
701 --------
702 >>> idx = pd.PeriodIndex(["2023-01", "2023-02", "2023-03"], freq="M")
703 >>> idx.days_in_month
704 Index([31, 28, 31], dtype='int64')
705 """
706 return self._wrap_field("days_in_month")
707
708 @property
709 def daysinmonth(self) -> Index:
710 """
711 The number of days in the month.
712
713 .. deprecated:: 3.1.0
714 Use :attr:`PeriodIndex.days_in_month` instead.
715 """
716 warnings.warn(
717 "PeriodIndex.daysinmonth is deprecated and will be removed in a "
718 "future version. Use PeriodIndex.days_in_month instead.",
719 Pandas4Warning,
720 stacklevel=find_stack_level(),
721 )
722 return self._wrap_field("days_in_month")
723
724 @property
725 def is_leap_year(self) -> npt.NDArray[np.bool_]:
726 """
727 Logical indicating if the date belongs to a leap year.
728
729 Returns a boolean array where ``True`` indicates the period's year
730 is a leap year.
731
732 See Also
733 --------
734 PeriodIndex.qyear : Fiscal year the Period lies in according to its
735 starting-quarter.
736 PeriodIndex.year : The year of the period.
737
738 Examples
739 --------
740 >>> idx = pd.PeriodIndex(["2023", "2024", "2025"], freq="Y")
741 >>> idx.is_leap_year
742 array([False, True, False])
743 """
744 return self._data.is_leap_year
745
746 # ------------------------------------------------------------------------
747 # Index Constructors
748
749 def __new__(
750 cls,
751 data=None,
752 freq=None,
753 dtype: Dtype | None = None,
754 copy: bool | None = None,
755 name: Hashable | None = None,
756 ) -> Self:
757 refs = None
758 if not copy and isinstance(data, (Index, ABCSeries)):
759 refs = data._references
760 if dtype is not None:
761 dtype = pandas_dtype(dtype)
762
763 name = maybe_extract_name(name, data, cls)
764
765 if freq is not None:
766 freq = to_offset(freq, is_period=True)
767 dtype2 = PeriodDtype(freq) if freq is not None else None
768 dtype = validate_dtype_freq(dtype, dtype2)
769 if dtype is not None:
770 freq = dtype._freq
771
772 # GH#63388
773 data, copy = cls._maybe_copy_array_input(data, copy, dtype)
774
775 data = period_array(data=data, dtype=dtype)
776
777 if copy:
778 data = data.copy()
779
780 return cls._simple_new(data, name=name, refs=refs)
781
782 @classmethod
783 def from_fields(
784 cls,
785 *,
786 year=None,
787 quarter=None,
788 month=None,
789 day=None,
790 hour=None,
791 minute=None,
792 second=None,
793 freq=None,
794 ) -> Self:
795 """
796 Construct a PeriodIndex from fields (year, month, day, etc.).
797
798 Each field (year, quarter, month, day, hour, minute, second) can be
799 specified as a scalar or list-like. At least one field must be
800 list-like; scalar fields are broadcast to its length. The frequency
801 is inferred from the fields provided or can be given explicitly.
802
803 Parameters
804 ----------
805 year : int or list-like, default None
806 Year for the PeriodIndex.
807 quarter : int or list-like, default None
808 Quarter for the PeriodIndex.
809 month : int or list-like, default None
810 Month for the PeriodIndex.
811 day : int or list-like, default None
812 Day for the PeriodIndex.
813 hour : int or list-like, default None
814 Hour for the PeriodIndex.
815 minute : int or list-like, default None
816 Minute for the PeriodIndex.
817 second : int or list-like, default None
818 Second for the PeriodIndex.
819 freq : str or period object, optional
820 One of pandas period strings or corresponding objects.
821
822 Returns
823 -------
824 PeriodIndex
825
826 See Also
827 --------
828 PeriodIndex.from_ordinals : Construct a PeriodIndex from ordinals.
829 PeriodIndex.to_timestamp : Cast to DatetimeArray/Index.
830
831 Notes
832 -----
833 A list-like field must be a ``list``, ``tuple``, ``np.ndarray``, or
834 ``Series``. Other array-like inputs, such as a ``range`` or an
835 ``Index``, are not accepted.
836
837 Examples
838 --------
839 >>> idx = pd.PeriodIndex.from_fields(year=[2000, 2002], quarter=[1, 3])
840 >>> idx
841 PeriodIndex(['2000Q1', '2002Q3'], dtype='period[Q-DEC]')
842 """
843 fields = {
844 "year": year,
845 "quarter": quarter,
846 "month": month,
847 "day": day,
848 "hour": hour,
849 "minute": minute,
850 "second": second,
851 }
852 fields = {key: value for key, value in fields.items() if value is not None}
853 arr = PeriodArray._from_fields(fields=fields, freq=freq)
854 return cls._simple_new(arr)
855
856 @classmethod
857 def from_ordinals(cls, ordinals, *, freq, name=None) -> Self:
858 """
859 Construct a PeriodIndex from ordinals.
860
861 Ordinals are integer offsets from the proleptic Gregorian epoch,
862 interpreted according to the given frequency.
863
864 Parameters
865 ----------
866 ordinals : array-like of int
867 The period offsets from the proleptic Gregorian epoch.
868 freq : str or period object
869 One of pandas period strings or corresponding objects.
870 name : str, default None
871 Name of the resulting PeriodIndex.
872
873 Returns
874 -------
875 PeriodIndex
876
877 See Also
878 --------
879 PeriodIndex.from_fields : Construct a PeriodIndex from fields
880 (year, month, day, etc.).
881 PeriodIndex.to_timestamp : Cast to DatetimeArray/Index.
882
883 Examples
884 --------
885 >>> idx = pd.PeriodIndex.from_ordinals([-1, 0, 1], freq="Q")
886 >>> idx
887 PeriodIndex(['1969Q4', '1970Q1', '1970Q2'], dtype='period[Q-DEC]')
888 """
889 ordinals = np.asarray(ordinals, dtype=np.int64)
890 dtype = PeriodDtype(freq)
891 data = PeriodArray._simple_new(ordinals, dtype=dtype)
892 return cls._simple_new(data, name=name)
893
894 # ------------------------------------------------------------------------
895 # Data
896
897 @property
898 def values(self) -> npt.NDArray[np.object_]:
899 warnings.warn(
900 "PeriodIndex.values returning an object-dtype ndarray is "
901 "deprecated. In a future version, this will return the "
902 "underlying PeriodArray instead. Use 'PeriodIndex.to_numpy()' "
903 "to get a NumPy array, or 'PeriodIndex.array' to get the "
904 "ExtensionArray.",
905 Pandas4Warning,
906 stacklevel=2,
907 )
908 return np.asarray(self, dtype=object)
909
910 def _mpl_repr(self) -> np.ndarray:
911 # Return ordinals directly so matplotlib receives numeric x-values,
912 # bypassing a round-trip through Period scalar objects. GH#10578
913 return self.asi8
914
915 def _maybe_convert_timedelta(self, other) -> int | npt.NDArray[np.int64]:
916 """
917 Convert timedelta-like input to an integer multiple of self.freq
918
919 Parameters
920 ----------
921 other : timedelta, np.timedelta64, DateOffset, int, np.ndarray
922
923 Returns
924 -------
925 converted : int, np.ndarray[int64]
926
927 Raises
928 ------
929 IncompatibleFrequency : if the input cannot be written as a multiple
930 of self.freq. Note IncompatibleFrequency subclasses ValueError.
931 """
932 if isinstance(other, (timedelta, np.timedelta64, Tick, np.ndarray)):
933 if isinstance(self.freq, (Tick, Day)):
934 # _check_timedeltalike_freq_compat will raise if incompatible
935 delta = self._data._check_timedeltalike_freq_compat(other)
936 return delta
937 elif isinstance(other, BaseOffset):
938 if other.base == self.freq.base:
939 return other.n
940
941 raise raise_on_incompatible(self, other)
942 elif is_integer(other):
943 assert isinstance(other, int)
944 return other
945
946 # raise when input doesn't have freq
947 raise raise_on_incompatible(self, None)
948
949 def _is_comparable_dtype(self, dtype: DtypeObj) -> bool:
950 """
951 Can we compare values of the given dtype to our own?
952 """
953 return self.dtype == dtype
954
955 # ------------------------------------------------------------------------
956 # Index Methods
957
958 def asof_locs(self, where: Index, mask: npt.NDArray[np.bool_]) -> np.ndarray:
959 """
960 where : array of timestamps
961 mask : np.ndarray[bool]
962 Array of booleans where data is not NA.
963 """
964 if isinstance(where, DatetimeIndex):
965 where = PeriodIndex(where._values, freq=self.freq, copy=False)
966 elif not isinstance(where, PeriodIndex):
967 raise TypeError("asof_locs `where` must be DatetimeIndex or PeriodIndex")
968
969 return super().asof_locs(where, mask)
970
971 @property
972 def is_full(self) -> bool:
973 """
974 Return True if the index contains all periods from start to end
975 (inclusive) with no gaps.
976
977 Requires monotonic increasing order. Duplicate periods are allowed.
978
979 .. deprecated:: 3.1.0
980 ``PeriodIndex.is_full`` is deprecated and will be removed in
981 a future version. Use
982 ``index.empty or len(index.unique()) ==
983 len(period_range(index.min(), index.max(), freq=index.freq))``
984 instead. Unlike ``is_full``, this does not raise on a
985 non-monotonic index.
986 """
987 warnings.warn(
988 "PeriodIndex.is_full is deprecated and will be removed in a "
989 "future version. Use index.empty or len(index.unique()) == "
990 "len(period_range(index.min(), index.max(), freq=index.freq)) "
991 "instead.",
992 Pandas4Warning,
993 stacklevel=find_stack_level(),
994 )
995 if len(self) == 0:
996 return True
997 if not self.is_monotonic_increasing:
998 raise ValueError("Index is not monotonic")
999 values = self.asi8
1000 return bool(((values[1:] - values[:-1]) < 2).all())
1001
1002 @property
1003 def inferred_type(self) -> str:
1004 # b/c data is represented as ints make sure we can't have ambiguous
1005 # indexing
1006 return "period"
1007
1008 # ------------------------------------------------------------------------
1009 # Indexing Methods
1010
1011 def _convert_tolerance(self, tolerance, target):
1012 # Returned tolerance must be in dtype/units so that
1013 # `|self._get_engine_target() - target._engine_target()| <= tolerance`
1014 # is meaningful. Since PeriodIndex returns int64 for engine_target,
1015 # we may need to convert timedelta64 tolerance to int64.
1016 tolerance = super()._convert_tolerance(tolerance, target)
1017
1018 if self.dtype == target.dtype:
1019 # convert tolerance to i8
1020 tolerance = self._maybe_convert_timedelta(tolerance)
1021
1022 return tolerance
1023
1024 def get_loc(self, key):
1025 """
1026 Get integer location for requested label.
1027
1028 Parameters
1029 ----------
1030 key : Period, NaT, str, or datetime
1031 String or datetime key must be parsable as Period.
1032
1033 Returns
1034 -------
1035 loc : int or ndarray[int64]
1036
1037 Raises
1038 ------
1039 KeyError
1040 Key is not present in the index.
1041 TypeError
1042 If key is listlike or otherwise not hashable.
1043 """
1044 orig_key = key
1045
1046 self._check_indexing_error(key)
1047
1048 if is_valid_na_for_dtype(key, self.dtype):
1049 key = NaT
1050
1051 elif isinstance(key, str):
1052 try:
1053 parsed, reso = self._parse_with_reso(key)
1054 except ValueError as err:
1055 # A string with invalid format
1056 raise KeyError(f"Cannot interpret '{key}' as period") from err
1057
1058 if self._can_partial_date_slice(reso):
1059 try:
1060 return self._partial_date_slice(reso, parsed)
1061 except KeyError as err:
1062 raise KeyError(key) from err
1063
1064 if reso == self._resolution_obj:
1065 # the reso < self._resolution_obj case goes
1066 # through _get_string_slice
1067 key = self._cast_partial_indexing_scalar(parsed)
1068 else:
1069 raise KeyError(key)
1070
1071 elif isinstance(key, Period):
1072 self._disallow_mismatched_indexing(key)
1073
1074 elif isinstance(key, datetime):
1075 key = self._cast_partial_indexing_scalar(key)
1076
1077 else:
1078 # in particular integer, which Period constructor would cast to string
1079 raise KeyError(key)
1080
1081 try:
1082 return Index.get_loc(self, key)
1083 except KeyError as err:
1084 raise KeyError(orig_key) from err
1085
1086 def _disallow_mismatched_indexing(self, key: Period) -> None:
1087 if key._dtype != self.dtype:
1088 raise KeyError(key)
1089
1090 def _cast_partial_indexing_scalar(self, label: datetime) -> Period:
1091 try:
1092 period = Period(label, freq=self.freq)
1093 except ValueError as err:
1094 # we cannot construct the Period
1095 raise KeyError(label) from err
1096 return period
1097
1098 def _maybe_cast_slice_bound(self, label, side: str):
1099 """
1100 If label is a string, cast it to scalar type according to resolution.
1101
1102 Parameters
1103 ----------
1104 label : object
1105 side : {'left', 'right'}
1106
1107 Returns
1108 -------
1109 label : object
1110
1111 Notes
1112 -----
1113 Value of `side` parameter should be validated in caller.
1114 """
1115 if isinstance(label, datetime):
1116 label = self._cast_partial_indexing_scalar(label)
1117
1118 return super()._maybe_cast_slice_bound(label, side)
1119
1120 def _parsed_string_to_bounds(self, reso: Resolution, parsed: datetime):
1121 freq = OFFSET_TO_PERIOD_FREQSTR.get(reso.attr_abbrev, reso.attr_abbrev)
1122 iv = Period(parsed, freq=freq)
1123 return (iv.asfreq(self.freq, how="start"), iv.asfreq(self.freq, how="end"))
1124
1125 def shift(self, periods: int = 1, freq=None) -> Self:
1126 """
1127 Shift index by desired number of time frequency increments.
1128
1129 This method is for shifting the values of datetime-like indexes
1130 by a specified time increment a given number of times.
1131
1132 Parameters
1133 ----------
1134 periods : int, default 1
1135 Number of periods (or increments) to shift by,
1136 can be positive or negative.
1137 freq : pandas.DateOffset, pandas.Timedelta or string, optional
1138 Frequency increment to shift by.
1139 If None, the index is shifted by its own `freq` attribute.
1140 Offset aliases are valid strings, e.g., 'D', 'W', 'M' etc.
1141
1142 Returns
1143 -------
1144 pandas.DatetimeIndex
1145 Shifted index.
1146
1147 See Also
1148 --------
1149 Index.shift : Shift values of Index.
1150 PeriodIndex.shift : Shift values of PeriodIndex.
1151 """
1152 if freq is not None:
1153 raise TypeError(
1154 f"`freq` argument is not supported for {type(self).__name__}.shift"
1155 )
1156 return self + periods
1157
1158
1159@set_module("pandas")
1160def period_range(
1161 start=None,
1162 end=None,
1163 periods: int | None = None,
1164 freq=None,
1165 name: Hashable | None = None,
1166) -> PeriodIndex:
1167 """
1168 Return a fixed frequency PeriodIndex.
1169
1170 The day (calendar) is the default frequency.
1171
1172 Parameters
1173 ----------
1174 start : str, datetime, date, pandas.Timestamp, or period-like, default None
1175 Left bound for generating periods.
1176 end : str, datetime, date, pandas.Timestamp, or period-like, default None
1177 Right bound for generating periods.
1178 periods : int, default None
1179 Number of periods to generate.
1180 freq : str or DateOffset, optional
1181 Frequency alias. By default the freq is taken from `start` or `end`
1182 if those are Period objects. Otherwise, the default is ``"D"`` for
1183 daily frequency.
1184 name : str, default None
1185 Name of the resulting PeriodIndex.
1186
1187 Returns
1188 -------
1189 PeriodIndex
1190 A PeriodIndex of fixed frequency periods.
1191
1192 See Also
1193 --------
1194 date_range : Returns a fixed frequency DatetimeIndex.
1195 Period : Represents a period of time.
1196 PeriodIndex : Immutable ndarray holding ordinal values indicating regular periods
1197 in time.
1198
1199 Notes
1200 -----
1201 Of the three parameters: ``start``, ``end``, and ``periods``, exactly two
1202 must be specified.
1203
1204 To learn more about the frequency strings, please see
1205 :ref:`this link<timeseries.offset_aliases>`.
1206
1207 Examples
1208 --------
1209 >>> pd.period_range(start="2017-01-01", end="2018-01-01", freq="M")
1210 PeriodIndex(['2017-01', '2017-02', '2017-03', '2017-04', '2017-05', '2017-06',
1211 '2017-07', '2017-08', '2017-09', '2017-10', '2017-11', '2017-12',
1212 '2018-01'],
1213 dtype='period[M]')
1214
1215 If ``start`` or ``end`` are ``Period`` objects, they will be used as anchor
1216 endpoints for a ``PeriodIndex`` with frequency matching that of the
1217 ``period_range`` constructor.
1218
1219 >>> pd.period_range(
1220 ... start=pd.Period("2017Q1", freq="Q"),
1221 ... end=pd.Period("2017Q2", freq="Q"),
1222 ... freq="M",
1223 ... )
1224 PeriodIndex(['2017-03', '2017-04', '2017-05', '2017-06'],
1225 dtype='period[M]')
1226 """
1227 if com.count_not_none(start, end, periods) != 2:
1228 raise ValueError(
1229 "Of the three parameters: start, end, and periods, "
1230 "exactly two must be specified"
1231 )
1232 if freq is None and (not isinstance(start, Period) and not isinstance(end, Period)):
1233 freq = "D"
1234
1235 data, freq = PeriodArray._generate_range(start, end, periods, freq)
1236 dtype = PeriodDtype(freq)
1237 data = PeriodArray(data, dtype=dtype)
1238 return PeriodIndex(data, name=name, copy=False)