Skip to content

Commit 55cd96a

Browse files
authored
DEPR: PeriodIndex ordinal, fields keywords (#55963)
* DEPR: PeriodIndex ordinal, fields keywords * lint fixups * update doctest * doc updates
1 parent d999aac commit 55cd96a

File tree

9 files changed

+145
-35
lines changed

9 files changed

+145
-35
lines changed

doc/source/reference/indexing.rst

+2
Original file line numberDiff line numberDiff line change
@@ -489,3 +489,5 @@ Methods
489489
PeriodIndex.asfreq
490490
PeriodIndex.strftime
491491
PeriodIndex.to_timestamp
492+
PeriodIndex.from_fields
493+
PeriodIndex.from_ordinals

doc/source/whatsnew/v2.2.0.rst

+2
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,7 @@ Other Deprecations
264264
- Changed :meth:`Timedelta.resolution_string` to return ``h``, ``min``, ``s``, ``ms``, ``us``, and ``ns`` instead of ``H``, ``T``, ``S``, ``L``, ``U``, and ``N``, for compatibility with respective deprecations in frequency aliases (:issue:`52536`)
265265
- Deprecated :func:`read_gbq` and :meth:`DataFrame.to_gbq`. Use ``pandas_gbq.read_gbq`` and ``pandas_gbq.to_gbq`` instead https://pandas-gbq.readthedocs.io/en/latest/api.html (:issue:`55525`)
266266
- Deprecated :meth:`Index.format`, use ``index.astype(str)`` or ``index.map(formatter)`` instead (:issue:`55413`)
267+
- Deprecated ``year``, ``month``, ``quarter``, ``day``, ``hour``, ``minute``, and ``second`` keywords in the :class:`PeriodIndex` constructor, use :meth:`PeriodIndex.from_fields` instead (:issue:`55960`)
267268
- Deprecated allowing non-keyword arguments in :meth:`DataFrame.to_clipboard`. (:issue:`54229`)
268269
- Deprecated allowing non-keyword arguments in :meth:`DataFrame.to_csv` except ``path_or_buf``. (:issue:`54229`)
269270
- Deprecated allowing non-keyword arguments in :meth:`DataFrame.to_dict`. (:issue:`54229`)
@@ -294,6 +295,7 @@ Other Deprecations
294295
- Deprecated strings ``T``, ``S``, ``L``, ``U``, and ``N`` denoting frequencies in :class:`Minute`, :class:`Second`, :class:`Milli`, :class:`Micro`, :class:`Nano` (:issue:`52536`)
295296
- Deprecated the ``errors="ignore"`` option in :func:`to_datetime`, :func:`to_timedelta`, and :func:`to_numeric`; explicitly catch exceptions instead (:issue:`54467`)
296297
- Deprecated the ``fastpath`` keyword in the :class:`Series` constructor (:issue:`20110`)
298+
- Deprecated the ``ordinal`` keyword in :class:`PeriodIndex`, use :meth:`PeriodIndex.from_ordinals` instead (:issue:`55960`)
297299
- Deprecated the extension test classes ``BaseNoReduceTests``, ``BaseBooleanReduceTests``, and ``BaseNumericReduceTests``, use ``BaseReduceTests`` instead (:issue:`54663`)
298300
- Deprecated the option ``mode.data_manager`` and the ``ArrayManager``; only the ``BlockManager`` will be available in future versions (:issue:`55043`)
299301
- Deprecated the previous implementation of :class:`DataFrame.stack`; specify ``future_stack=True`` to adopt the future version (:issue:`53515`)

pandas/core/arrays/period.py

+7-8
Original file line numberDiff line numberDiff line change
@@ -329,26 +329,25 @@ def _from_datetime64(cls, data, freq, tz=None) -> Self:
329329
return cls(data, dtype=dtype)
330330

331331
@classmethod
332-
def _generate_range(cls, start, end, periods, freq, fields):
332+
def _generate_range(cls, start, end, periods, freq):
333333
periods = dtl.validate_periods(periods)
334334

335335
if freq is not None:
336336
freq = Period._maybe_convert_freq(freq)
337337

338-
field_count = len(fields)
339338
if start is not None or end is not None:
340-
if field_count > 0:
341-
raise ValueError(
342-
"Can either instantiate from fields or endpoints, but not both"
343-
)
344339
subarr, freq = _get_ordinal_range(start, end, periods, freq)
345-
elif field_count > 0:
346-
subarr, freq = _range_from_fields(freq=freq, **fields)
347340
else:
348341
raise ValueError("Not enough parameters to construct Period range")
349342

350343
return subarr, freq
351344

345+
@classmethod
346+
def _from_fields(cls, *, fields: dict, freq) -> Self:
347+
subarr, freq = _range_from_fields(freq=freq, **fields)
348+
dtype = PeriodDtype(freq)
349+
return cls._simple_new(subarr, dtype=dtype)
350+
352351
# -----------------------------------------------------------------
353352
# DatetimeLike Interface
354353

pandas/core/indexes/period.py

+80-9
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
timedelta,
66
)
77
from typing import TYPE_CHECKING
8+
import warnings
89

910
import numpy as np
1011

@@ -21,6 +22,7 @@
2122
cache_readonly,
2223
doc,
2324
)
25+
from pandas.util._exceptions import find_stack_level
2426

2527
from pandas.core.dtypes.common import is_integer
2628
from pandas.core.dtypes.dtypes import PeriodDtype
@@ -97,12 +99,33 @@ class PeriodIndex(DatetimeIndexOpsMixin):
9799
freq : str or period object, optional
98100
One of pandas period strings or corresponding objects.
99101
year : int, array, or Series, default None
102+
103+
.. deprecated:: 2.2.0
104+
Use PeriodIndex.from_fields instead.
100105
month : int, array, or Series, default None
106+
107+
.. deprecated:: 2.2.0
108+
Use PeriodIndex.from_fields instead.
101109
quarter : int, array, or Series, default None
110+
111+
.. deprecated:: 2.2.0
112+
Use PeriodIndex.from_fields instead.
102113
day : int, array, or Series, default None
114+
115+
.. deprecated:: 2.2.0
116+
Use PeriodIndex.from_fields instead.
103117
hour : int, array, or Series, default None
118+
119+
.. deprecated:: 2.2.0
120+
Use PeriodIndex.from_fields instead.
104121
minute : int, array, or Series, default None
122+
123+
.. deprecated:: 2.2.0
124+
Use PeriodIndex.from_fields instead.
105125
second : int, array, or Series, default None
126+
127+
.. deprecated:: 2.2.0
128+
Use PeriodIndex.from_fields instead.
106129
dtype : str or PeriodDtype, default None
107130
108131
Attributes
@@ -135,6 +158,8 @@ class PeriodIndex(DatetimeIndexOpsMixin):
135158
asfreq
136159
strftime
137160
to_timestamp
161+
from_fields
162+
from_ordinals
138163
139164
See Also
140165
--------
@@ -146,7 +171,7 @@ class PeriodIndex(DatetimeIndexOpsMixin):
146171
147172
Examples
148173
--------
149-
>>> idx = pd.PeriodIndex(year=[2000, 2002], quarter=[1, 3])
174+
>>> idx = pd.PeriodIndex.from_fields(year=[2000, 2002], quarter=[1, 3])
150175
>>> idx
151176
PeriodIndex(['2000Q1', '2002Q3'], dtype='period[Q-DEC]')
152177
"""
@@ -233,6 +258,24 @@ def __new__(
233258
if not set(fields).issubset(valid_field_set):
234259
argument = next(iter(set(fields) - valid_field_set))
235260
raise TypeError(f"__new__() got an unexpected keyword argument {argument}")
261+
elif len(fields):
262+
# GH#55960
263+
warnings.warn(
264+
"Constructing PeriodIndex from fields is deprecated. Use "
265+
"PeriodIndex.from_fields instead.",
266+
FutureWarning,
267+
stacklevel=find_stack_level(),
268+
)
269+
270+
if ordinal is not None:
271+
# GH#55960
272+
warnings.warn(
273+
"The 'ordinal' keyword in PeriodIndex is deprecated and will "
274+
"be removed in a future version. Use PeriodIndex.from_ordinals "
275+
"instead.",
276+
FutureWarning,
277+
stacklevel=find_stack_level(),
278+
)
236279

237280
name = maybe_extract_name(name, data, cls)
238281

@@ -241,14 +284,9 @@ def __new__(
241284
if not fields:
242285
# test_pickle_compat_construction
243286
cls._raise_scalar_data_error(None)
287+
data = cls.from_fields(**fields, freq=freq)._data
288+
copy = False
244289

245-
data, freq2 = PeriodArray._generate_range(None, None, None, freq, fields)
246-
# PeriodArray._generate range does validation that fields is
247-
# empty when really using the range-based constructor.
248-
freq = freq2
249-
250-
dtype = PeriodDtype(freq)
251-
data = PeriodArray(data, dtype=dtype)
252290
elif fields:
253291
if data is not None:
254292
raise ValueError("Cannot pass both data and fields")
@@ -280,6 +318,39 @@ def __new__(
280318

281319
return cls._simple_new(data, name=name, refs=refs)
282320

321+
@classmethod
322+
def from_fields(
323+
cls,
324+
*,
325+
year=None,
326+
quarter=None,
327+
month=None,
328+
day=None,
329+
hour=None,
330+
minute=None,
331+
second=None,
332+
freq=None,
333+
) -> Self:
334+
fields = {
335+
"year": year,
336+
"quarter": quarter,
337+
"month": month,
338+
"day": day,
339+
"hour": hour,
340+
"minute": minute,
341+
"second": second,
342+
}
343+
fields = {key: value for key, value in fields.items() if value is not None}
344+
arr = PeriodArray._from_fields(fields=fields, freq=freq)
345+
return cls._simple_new(arr)
346+
347+
@classmethod
348+
def from_ordinals(cls, ordinals, *, freq, name=None) -> Self:
349+
ordinals = np.asarray(ordinals, dtype=np.int64)
350+
dtype = PeriodDtype(freq)
351+
data = PeriodArray._simple_new(ordinals, dtype=dtype)
352+
return cls._simple_new(data, name=name)
353+
283354
# ------------------------------------------------------------------------
284355
# Data
285356

@@ -537,7 +608,7 @@ def period_range(
537608
if freq is None and (not isinstance(start, Period) and not isinstance(end, Period)):
538609
freq = "D"
539610

540-
data, freq = PeriodArray._generate_range(start, end, periods, freq, fields={})
611+
data, freq = PeriodArray._generate_range(start, end, periods, freq)
541612
dtype = PeriodDtype(freq)
542613
data = PeriodArray(data, dtype=dtype)
543614
return PeriodIndex(data, name=name)

pandas/io/pytables.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -2169,8 +2169,10 @@ def convert(
21692169
# error: Incompatible types in assignment (expression has type
21702170
# "Callable[[Any, KwArg(Any)], PeriodIndex]", variable has type
21712171
# "Union[Type[Index], Type[DatetimeIndex]]")
2172-
factory = lambda x, **kwds: PeriodIndex( # type: ignore[assignment]
2173-
ordinal=x, **kwds
2172+
factory = lambda x, **kwds: PeriodIndex.from_ordinals( # type: ignore[assignment]
2173+
x, freq=kwds.get("freq", None)
2174+
)._rename(
2175+
kwds["name"]
21742176
)
21752177

21762178
# making an Index instance could throw a number of different errors

pandas/tests/indexes/period/methods/test_to_timestamp.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ def test_to_timestamp_quarterly_bug(self):
8787
years = np.arange(1960, 2000).repeat(4)
8888
quarters = np.tile(list(range(1, 5)), 40)
8989

90-
pindex = PeriodIndex(year=years, quarter=quarters)
90+
pindex = PeriodIndex.from_fields(year=years, quarter=quarters)
9191

9292
stamps = pindex.to_timestamp("D", "end")
9393
expected = DatetimeIndex([x.to_timestamp("D", "end") for x in pindex])

pandas/tests/indexes/period/test_constructors.py

+35-12
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,23 @@ class TestPeriodIndex:
2323
def test_keyword_mismatch(self):
2424
# GH#55961 we should get exactly one of data/ordinals/**fields
2525
per = Period("2016-01-01", "D")
26+
depr_msg1 = "The 'ordinal' keyword in PeriodIndex is deprecated"
27+
depr_msg2 = "Constructing PeriodIndex from fields is deprecated"
2628

2729
err_msg1 = "Cannot pass both data and ordinal"
2830
with pytest.raises(ValueError, match=err_msg1):
29-
PeriodIndex(data=[per], ordinal=[per.ordinal], freq=per.freq)
31+
with tm.assert_produces_warning(FutureWarning, match=depr_msg1):
32+
PeriodIndex(data=[per], ordinal=[per.ordinal], freq=per.freq)
3033

3134
err_msg2 = "Cannot pass both data and fields"
3235
with pytest.raises(ValueError, match=err_msg2):
33-
PeriodIndex(data=[per], year=[per.year], freq=per.freq)
36+
with tm.assert_produces_warning(FutureWarning, match=depr_msg2):
37+
PeriodIndex(data=[per], year=[per.year], freq=per.freq)
3438

3539
err_msg3 = "Cannot pass both ordinal and fields"
3640
with pytest.raises(ValueError, match=err_msg3):
37-
PeriodIndex(ordinal=[per.ordinal], year=[per.year], freq=per.freq)
41+
with tm.assert_produces_warning(FutureWarning, match=depr_msg2):
42+
PeriodIndex(ordinal=[per.ordinal], year=[per.year], freq=per.freq)
3843

3944
def test_construction_base_constructor(self):
4045
# GH 13664
@@ -94,28 +99,35 @@ def test_constructor_field_arrays(self):
9499
years = np.arange(1990, 2010).repeat(4)[2:-2]
95100
quarters = np.tile(np.arange(1, 5), 20)[2:-2]
96101

97-
index = PeriodIndex(year=years, quarter=quarters, freq="Q-DEC")
102+
depr_msg = "Constructing PeriodIndex from fields is deprecated"
103+
with tm.assert_produces_warning(FutureWarning, match=depr_msg):
104+
index = PeriodIndex(year=years, quarter=quarters, freq="Q-DEC")
98105
expected = period_range("1990Q3", "2009Q2", freq="Q-DEC")
99106
tm.assert_index_equal(index, expected)
100107

101-
index2 = PeriodIndex(year=years, quarter=quarters, freq="2Q-DEC")
108+
with tm.assert_produces_warning(FutureWarning, match=depr_msg):
109+
index2 = PeriodIndex(year=years, quarter=quarters, freq="2Q-DEC")
102110
tm.assert_numpy_array_equal(index.asi8, index2.asi8)
103111

104-
index = PeriodIndex(year=years, quarter=quarters)
112+
with tm.assert_produces_warning(FutureWarning, match=depr_msg):
113+
index = PeriodIndex(year=years, quarter=quarters)
105114
tm.assert_index_equal(index, expected)
106115

107116
years = [2007, 2007, 2007]
108117
months = [1, 2]
109118

110119
msg = "Mismatched Period array lengths"
111120
with pytest.raises(ValueError, match=msg):
112-
PeriodIndex(year=years, month=months, freq="M")
121+
with tm.assert_produces_warning(FutureWarning, match=depr_msg):
122+
PeriodIndex(year=years, month=months, freq="M")
113123
with pytest.raises(ValueError, match=msg):
114-
PeriodIndex(year=years, month=months, freq="2M")
124+
with tm.assert_produces_warning(FutureWarning, match=depr_msg):
125+
PeriodIndex(year=years, month=months, freq="2M")
115126

116127
years = [2007, 2007, 2007]
117128
months = [1, 2, 3]
118-
idx = PeriodIndex(year=years, month=months, freq="M")
129+
with tm.assert_produces_warning(FutureWarning, match=depr_msg):
130+
idx = PeriodIndex(year=years, month=months, freq="M")
119131
exp = period_range("2007-01", periods=3, freq="M")
120132
tm.assert_index_equal(idx, exp)
121133

@@ -145,15 +157,24 @@ def test_constructor_arrays_negative_year(self):
145157
years = np.arange(1960, 2000, dtype=np.int64).repeat(4)
146158
quarters = np.tile(np.array([1, 2, 3, 4], dtype=np.int64), 40)
147159

148-
pindex = PeriodIndex(year=years, quarter=quarters)
160+
msg = "Constructing PeriodIndex from fields is deprecated"
161+
with tm.assert_produces_warning(FutureWarning, match=msg):
162+
pindex = PeriodIndex(year=years, quarter=quarters)
149163

150164
tm.assert_index_equal(pindex.year, Index(years))
151165
tm.assert_index_equal(pindex.quarter, Index(quarters))
152166

167+
alt = PeriodIndex.from_fields(year=years, quarter=quarters)
168+
tm.assert_index_equal(alt, pindex)
169+
153170
def test_constructor_invalid_quarters(self):
171+
depr_msg = "Constructing PeriodIndex from fields is deprecated"
154172
msg = "Quarter must be 1 <= q <= 4"
155173
with pytest.raises(ValueError, match=msg):
156-
PeriodIndex(year=range(2000, 2004), quarter=list(range(4)), freq="Q-DEC")
174+
with tm.assert_produces_warning(FutureWarning, match=depr_msg):
175+
PeriodIndex(
176+
year=range(2000, 2004), quarter=list(range(4)), freq="Q-DEC"
177+
)
157178

158179
def test_constructor_corner(self):
159180
result = period_range("2007-01", periods=10.5, freq="M")
@@ -394,7 +415,9 @@ def test_constructor_nat(self):
394415
def test_constructor_year_and_quarter(self):
395416
year = Series([2001, 2002, 2003])
396417
quarter = year - 2000
397-
idx = PeriodIndex(year=year, quarter=quarter)
418+
msg = "Constructing PeriodIndex from fields is deprecated"
419+
with tm.assert_produces_warning(FutureWarning, match=msg):
420+
idx = PeriodIndex(year=year, quarter=quarter)
398421
strs = [f"{t[0]:d}Q{t[1]:d}" for t in zip(quarter, year)]
399422
lops = list(map(Period, strs))
400423
p = PeriodIndex(lops)

pandas/tests/indexes/period/test_period.py

+11-2
Original file line numberDiff line numberDiff line change
@@ -214,10 +214,19 @@ def test_negative_ordinals(self):
214214
Period(ordinal=-1000, freq="Y")
215215
Period(ordinal=0, freq="Y")
216216

217-
idx1 = PeriodIndex(ordinal=[-1, 0, 1], freq="Y")
218-
idx2 = PeriodIndex(ordinal=np.array([-1, 0, 1]), freq="Y")
217+
msg = "The 'ordinal' keyword in PeriodIndex is deprecated"
218+
with tm.assert_produces_warning(FutureWarning, match=msg):
219+
idx1 = PeriodIndex(ordinal=[-1, 0, 1], freq="Y")
220+
with tm.assert_produces_warning(FutureWarning, match=msg):
221+
idx2 = PeriodIndex(ordinal=np.array([-1, 0, 1]), freq="Y")
219222
tm.assert_index_equal(idx1, idx2)
220223

224+
alt1 = PeriodIndex.from_ordinals([-1, 0, 1], freq="Y")
225+
tm.assert_index_equal(alt1, idx1)
226+
227+
alt2 = PeriodIndex.from_ordinals(np.array([-1, 0, 1]), freq="Y")
228+
tm.assert_index_equal(alt2, idx2)
229+
221230
def test_pindex_fieldaccessor_nat(self):
222231
idx = PeriodIndex(
223232
["2011-01", "2011-02", "NaT", "2012-03", "2012-04"], freq="D", name="name"

pandas/tests/indexes/test_old_base.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,9 @@ def test_ensure_copied_data(self, index):
272272

273273
if isinstance(index, PeriodIndex):
274274
# .values an object array of Period, thus copied
275-
result = index_type(ordinal=index.asi8, copy=False, **init_kwargs)
275+
depr_msg = "The 'ordinal' keyword in PeriodIndex is deprecated"
276+
with tm.assert_produces_warning(FutureWarning, match=depr_msg):
277+
result = index_type(ordinal=index.asi8, copy=False, **init_kwargs)
276278
tm.assert_numpy_array_equal(index.asi8, result.asi8, check_same="same")
277279
elif isinstance(index, IntervalIndex):
278280
# checked in test_interval.py

0 commit comments

Comments
 (0)