Skip to content

DEPR: PeriodIndex ordinal, fields keywords #55963

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Nov 15, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions doc/source/whatsnew/v2.2.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ Other Deprecations
- 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`)
- 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`)
- Deprecated :meth:`Index.format`, use ``index.astype(str)`` or ``index.map(formatter)`` instead (:issue:`55413`)
- Deprecated ``year``, ``month``, ``quarter``, ``day``, ``hour``, ``minute``, and ``second`` keywords in the :class:`PeriodIndex` constructor, use :meth:`PeriodIndex.from_fields` instead (:issue:`55960`)
- Deprecated allowing non-keyword arguments in :meth:`DataFrame.to_clipboard`. (:issue:`54229`)
- Deprecated allowing non-keyword arguments in :meth:`DataFrame.to_csv` except ``path_or_buf``. (:issue:`54229`)
- Deprecated allowing non-keyword arguments in :meth:`DataFrame.to_dict`. (:issue:`54229`)
Expand Down Expand Up @@ -294,6 +295,7 @@ Other Deprecations
- Deprecated strings ``T``, ``S``, ``L``, ``U``, and ``N`` denoting frequencies in :class:`Minute`, :class:`Second`, :class:`Milli`, :class:`Micro`, :class:`Nano` (:issue:`52536`)
- Deprecated the ``errors="ignore"`` option in :func:`to_datetime`, :func:`to_timedelta`, and :func:`to_numeric`; explicitly catch exceptions instead (:issue:`54467`)
- Deprecated the ``fastpath`` keyword in the :class:`Series` constructor (:issue:`20110`)
- Deprecated the ``ordinal`` keyword in :class:`PeriodIndex`, use :meth:`PeriodIndex.from_ordinals` instead (:issue:`55960`)
- Deprecated the extension test classes ``BaseNoReduceTests``, ``BaseBooleanReduceTests``, and ``BaseNumericReduceTests``, use ``BaseReduceTests`` instead (:issue:`54663`)
- Deprecated the option ``mode.data_manager`` and the ``ArrayManager``; only the ``BlockManager`` will be available in future versions (:issue:`55043`)
- Deprecated the previous implementation of :class:`DataFrame.stack`; specify ``future_stack=True`` to adopt the future version (:issue:`53515`)
Expand Down
15 changes: 7 additions & 8 deletions pandas/core/arrays/period.py
Original file line number Diff line number Diff line change
Expand Up @@ -329,26 +329,25 @@ def _from_datetime64(cls, data, freq, tz=None) -> Self:
return cls(data, dtype=dtype)

@classmethod
def _generate_range(cls, start, end, periods, freq, fields):
def _generate_range(cls, start, end, periods, freq):
periods = dtl.validate_periods(periods)

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

field_count = len(fields)
if start is not None or end is not None:
if field_count > 0:
raise ValueError(
"Can either instantiate from fields or endpoints, but not both"
)
subarr, freq = _get_ordinal_range(start, end, periods, freq)
elif field_count > 0:
subarr, freq = _range_from_fields(freq=freq, **fields)
else:
raise ValueError("Not enough parameters to construct Period range")

return subarr, freq

@classmethod
def _from_fields(cls, *, fields: dict, freq) -> Self:
subarr, freq = _range_from_fields(freq=freq, **fields)
dtype = PeriodDtype(freq)
return cls._simple_new(subarr, dtype=dtype)

# -----------------------------------------------------------------
# DatetimeLike Interface

Expand Down
66 changes: 57 additions & 9 deletions pandas/core/indexes/period.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
timedelta,
)
from typing import TYPE_CHECKING
import warnings

import numpy as np

Expand All @@ -21,6 +22,7 @@
cache_readonly,
doc,
)
from pandas.util._exceptions import find_stack_level

from pandas.core.dtypes.common import is_integer
from pandas.core.dtypes.dtypes import PeriodDtype
Expand Down Expand Up @@ -146,7 +148,7 @@ class PeriodIndex(DatetimeIndexOpsMixin):

Examples
--------
>>> idx = pd.PeriodIndex(year=[2000, 2002], quarter=[1, 3])
>>> idx = pd.PeriodIndex.from_fields(year=[2000, 2002], quarter=[1, 3])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add a .. deprecated 2.2.0 directive in the docstring for the fields?

>>> idx
PeriodIndex(['2000Q1', '2002Q3'], dtype='period[Q-DEC]')
"""
Expand Down Expand Up @@ -233,6 +235,24 @@ def __new__(
if not set(fields).issubset(valid_field_set):
argument = next(iter(set(fields) - valid_field_set))
raise TypeError(f"__new__() got an unexpected keyword argument {argument}")
elif len(fields):
# GH#55960
warnings.warn(
"Constructing PeriodIndex from fields is deprecated. Use "
"PeriodIndex.from_fields instead.",
FutureWarning,
stacklevel=find_stack_level(),
)

if ordinal is not None:
# GH#55960
warnings.warn(
"The 'ordinal' keyword in PeriodIndex is deprecated and will "
"be removed in a future version. Use PeriodIndex.from_ordinals "
"instead.",
FutureWarning,
stacklevel=find_stack_level(),
)

name = maybe_extract_name(name, data, cls)

Expand All @@ -241,14 +261,9 @@ def __new__(
if not fields:
# test_pickle_compat_construction
cls._raise_scalar_data_error(None)
data = cls.from_fields(**fields, freq=freq)._data
copy = False

data, freq2 = PeriodArray._generate_range(None, None, None, freq, fields)
# PeriodArray._generate range does validation that fields is
# empty when really using the range-based constructor.
freq = freq2

dtype = PeriodDtype(freq)
data = PeriodArray(data, dtype=dtype)
elif fields:
if data is not None:
raise ValueError("Cannot pass both data and fields")
Expand Down Expand Up @@ -280,6 +295,39 @@ def __new__(

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

@classmethod
def from_fields(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add this method and from_ordinals to the API reference?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated+green

cls,
*,
year=None,
quarter=None,
month=None,
day=None,
hour=None,
minute=None,
second=None,
freq=None,
) -> Self:
fields = {
"year": year,
"quarter": quarter,
"month": month,
"day": day,
"hour": hour,
"minute": minute,
"second": second,
}
fields = {key: value for key, value in fields.items() if value is not None}
arr = PeriodArray._from_fields(fields=fields, freq=freq)
return cls._simple_new(arr)

@classmethod
def from_ordinals(cls, ordinals, *, freq, name=None) -> Self:
ordinals = np.asarray(ordinals, dtype=np.int64)
dtype = PeriodDtype(freq)
data = PeriodArray._simple_new(ordinals, dtype=dtype)
return cls._simple_new(data, name=name)

# ------------------------------------------------------------------------
# Data

Expand Down Expand Up @@ -537,7 +585,7 @@ def period_range(
if freq is None and (not isinstance(start, Period) and not isinstance(end, Period)):
freq = "D"

data, freq = PeriodArray._generate_range(start, end, periods, freq, fields={})
data, freq = PeriodArray._generate_range(start, end, periods, freq)
dtype = PeriodDtype(freq)
data = PeriodArray(data, dtype=dtype)
return PeriodIndex(data, name=name)
6 changes: 4 additions & 2 deletions pandas/io/pytables.py
Original file line number Diff line number Diff line change
Expand Up @@ -2169,8 +2169,10 @@ def convert(
# error: Incompatible types in assignment (expression has type
# "Callable[[Any, KwArg(Any)], PeriodIndex]", variable has type
# "Union[Type[Index], Type[DatetimeIndex]]")
factory = lambda x, **kwds: PeriodIndex( # type: ignore[assignment]
ordinal=x, **kwds
factory = lambda x, **kwds: PeriodIndex.from_ordinals( # type: ignore[assignment]
x, freq=kwds.get("freq", None)
)._rename(
kwds["name"]
)

# making an Index instance could throw a number of different errors
Expand Down
2 changes: 1 addition & 1 deletion pandas/tests/indexes/period/methods/test_to_timestamp.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ def test_to_timestamp_quarterly_bug(self):
years = np.arange(1960, 2000).repeat(4)
quarters = np.tile(list(range(1, 5)), 40)

pindex = PeriodIndex(year=years, quarter=quarters)
pindex = PeriodIndex.from_fields(year=years, quarter=quarters)

stamps = pindex.to_timestamp("D", "end")
expected = DatetimeIndex([x.to_timestamp("D", "end") for x in pindex])
Expand Down
47 changes: 35 additions & 12 deletions pandas/tests/indexes/period/test_constructors.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,23 @@ class TestPeriodIndex:
def test_keyword_mismatch(self):
# GH#55961 we should get exactly one of data/ordinals/**fields
per = Period("2016-01-01", "D")
depr_msg1 = "The 'ordinal' keyword in PeriodIndex is deprecated"
depr_msg2 = "Constructing PeriodIndex from fields is deprecated"

err_msg1 = "Cannot pass both data and ordinal"
with pytest.raises(ValueError, match=err_msg1):
PeriodIndex(data=[per], ordinal=[per.ordinal], freq=per.freq)
with tm.assert_produces_warning(FutureWarning, match=depr_msg1):
PeriodIndex(data=[per], ordinal=[per.ordinal], freq=per.freq)

err_msg2 = "Cannot pass both data and fields"
with pytest.raises(ValueError, match=err_msg2):
PeriodIndex(data=[per], year=[per.year], freq=per.freq)
with tm.assert_produces_warning(FutureWarning, match=depr_msg2):
PeriodIndex(data=[per], year=[per.year], freq=per.freq)

err_msg3 = "Cannot pass both ordinal and fields"
with pytest.raises(ValueError, match=err_msg3):
PeriodIndex(ordinal=[per.ordinal], year=[per.year], freq=per.freq)
with tm.assert_produces_warning(FutureWarning, match=depr_msg2):
PeriodIndex(ordinal=[per.ordinal], year=[per.year], freq=per.freq)

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

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

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

index = PeriodIndex(year=years, quarter=quarters)
with tm.assert_produces_warning(FutureWarning, match=depr_msg):
index = PeriodIndex(year=years, quarter=quarters)
tm.assert_index_equal(index, expected)

years = [2007, 2007, 2007]
months = [1, 2]

msg = "Mismatched Period array lengths"
with pytest.raises(ValueError, match=msg):
PeriodIndex(year=years, month=months, freq="M")
with tm.assert_produces_warning(FutureWarning, match=depr_msg):
PeriodIndex(year=years, month=months, freq="M")
with pytest.raises(ValueError, match=msg):
PeriodIndex(year=years, month=months, freq="2M")
with tm.assert_produces_warning(FutureWarning, match=depr_msg):
PeriodIndex(year=years, month=months, freq="2M")

years = [2007, 2007, 2007]
months = [1, 2, 3]
idx = PeriodIndex(year=years, month=months, freq="M")
with tm.assert_produces_warning(FutureWarning, match=depr_msg):
idx = PeriodIndex(year=years, month=months, freq="M")
exp = period_range("2007-01", periods=3, freq="M")
tm.assert_index_equal(idx, exp)

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

pindex = PeriodIndex(year=years, quarter=quarters)
msg = "Constructing PeriodIndex from fields is deprecated"
with tm.assert_produces_warning(FutureWarning, match=msg):
pindex = PeriodIndex(year=years, quarter=quarters)

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

alt = PeriodIndex.from_fields(year=years, quarter=quarters)
tm.assert_index_equal(alt, pindex)

def test_constructor_invalid_quarters(self):
depr_msg = "Constructing PeriodIndex from fields is deprecated"
msg = "Quarter must be 1 <= q <= 4"
with pytest.raises(ValueError, match=msg):
PeriodIndex(year=range(2000, 2004), quarter=list(range(4)), freq="Q-DEC")
with tm.assert_produces_warning(FutureWarning, match=depr_msg):
PeriodIndex(
year=range(2000, 2004), quarter=list(range(4)), freq="Q-DEC"
)

def test_constructor_corner(self):
result = period_range("2007-01", periods=10.5, freq="M")
Expand Down Expand Up @@ -394,7 +415,9 @@ def test_constructor_nat(self):
def test_constructor_year_and_quarter(self):
year = Series([2001, 2002, 2003])
quarter = year - 2000
idx = PeriodIndex(year=year, quarter=quarter)
msg = "Constructing PeriodIndex from fields is deprecated"
with tm.assert_produces_warning(FutureWarning, match=msg):
idx = PeriodIndex(year=year, quarter=quarter)
strs = [f"{t[0]:d}Q{t[1]:d}" for t in zip(quarter, year)]
lops = list(map(Period, strs))
p = PeriodIndex(lops)
Expand Down
13 changes: 11 additions & 2 deletions pandas/tests/indexes/period/test_period.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,10 +214,19 @@ def test_negative_ordinals(self):
Period(ordinal=-1000, freq="Y")
Period(ordinal=0, freq="Y")

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

alt1 = PeriodIndex.from_ordinals([-1, 0, 1], freq="Y")
tm.assert_index_equal(alt1, idx1)

alt2 = PeriodIndex.from_ordinals(np.array([-1, 0, 1]), freq="Y")
tm.assert_index_equal(alt2, idx2)

def test_pindex_fieldaccessor_nat(self):
idx = PeriodIndex(
["2011-01", "2011-02", "NaT", "2012-03", "2012-04"], freq="D", name="name"
Expand Down
4 changes: 3 additions & 1 deletion pandas/tests/indexes/test_old_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,9 @@ def test_ensure_copied_data(self, index):

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