Skip to content

Commit 31757f6

Browse files
jbrockmendelclaude
andcommitted
DEPR: Deprecate passing integer-dtype data to PeriodArray/PeriodIndex (pandas-dev#64227)
In a future version, integer data will be treated as period ordinals instead of year values, consistent with DatetimeArray behavior and the EA protocol. During the deprecation period, the current behavior is preserved with a Pandas4Warning. closes pandas-dev#64227 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 48295cd commit 31757f6

12 files changed

Lines changed: 70 additions & 45 deletions

File tree

doc/source/whatsnew/v3.1.0.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ Deprecations
122122
:meth:`DataFrame.var`, :meth:`DataFrame.sem`, :meth:`DataFrame.sum`,
123123
:meth:`DataFrame.prod` (and their :class:`Series` equivalents); this will raise in
124124
a future version of pandas (:issue:`53098`)
125+
- Deprecated passing integer-dtype data to :class:`PeriodIndex`, :func:`period_array`, and :meth:`PeriodArray._from_sequence`. In a future version, integers will be treated as period ordinals instead of year values. Use strings instead, e.g. ``PeriodIndex(["2020", "2021"], freq="Y")`` (:issue:`64227`)
125126
- Deprecated the inference of ``datetime64`` dtype from data containing ``datetime.date`` objects when used in comparisons or :meth:`Index.equals` with :class:`DatetimeIndex`. Use :func:`pandas.to_datetime` to explicitly convert to ``datetime64`` instead (:issue:`65056`)
126127
-
127128

pandas/core/arrays/period.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,16 @@ def _from_sequence(
302302
return cls(ordinals, dtype=dtype)
303303

304304
elif arrdata.dtype.kind in "iu":
305+
warnings.warn(
306+
"Passing integer-dtype data to PeriodArray/PeriodIndex is "
307+
"deprecated. In a future version, integer data will be treated "
308+
"as period ordinals instead of year values. To retain the "
309+
"current behavior, use "
310+
"PeriodIndex(data.astype(str), freq=...) or explicitly "
311+
"construct Period objects.",
312+
Pandas4Warning,
313+
stacklevel=find_stack_level(),
314+
)
305315
arr = arrdata.astype(np.int64, copy=False)
306316
ordinals = libperiod.from_calendar_ordinals(arr, dtype) # type: ignore[arg-type]
307317
return cls(ordinals, dtype=dtype)
@@ -1363,13 +1373,6 @@ def period_array(
13631373
['2017', '2018', 'NaT']
13641374
Length: 3, dtype: period[Y-DEC]
13651375
1366-
Integers that look like years are handled
1367-
1368-
>>> period_array([2000, 2001, 2002], dtype=PeriodDtype("D"))
1369-
<PeriodArray>
1370-
['2000-01-01', '2001-01-01', '2002-01-01']
1371-
Length: 3, dtype: period[D]
1372-
13731376
Datetime-like strings may also be passed
13741377
13751378
>>> period_array(

pandas/tests/arrays/period/test_constructors.py

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from pandas._libs.tslibs import iNaT
55
from pandas._libs.tslibs.offsets import MonthEnd
66
from pandas._libs.tslibs.period import IncompatibleFrequency
7+
from pandas.errors import Pandas4Warning
78

89
import pandas as pd
910
import pandas._testing as tm
@@ -18,7 +19,6 @@
1819
[
1920
([pd.Period("2017", "D")], None, [17167]),
2021
([pd.Period("2017", "D")], "D", [17167]),
21-
([2017], "D", [17167]),
2222
(["2017"], "D", [17167]),
2323
([pd.Period("2017", "D")], pd.tseries.offsets.Day(), [17167]),
2424
([pd.Period("2017", "D"), None], None, [17167, iNaT]),
@@ -111,25 +111,34 @@ def test_period_array_freq_mismatch():
111111
PeriodArray(arr, dtype=dtype)
112112

113113

114-
def test_from_sequence_allows_i8():
115-
# GH#64227 this used to be allowed for PeriodIndex and period_array
116-
# but not PeriodArray._from_sequence
114+
def test_from_sequence_int_deprecation():
115+
# GH#64227 passing integer data is deprecated; integers will be treated
116+
# as period ordinals in a future version instead of year values.
117117
arr = period_array(["1975", "1976"], dtype="period[D]")
118118

119-
expected = pd.PeriodIndex([pd.Period(x, freq=arr.freq) for x in arr.asi8]).array
120-
121-
result1 = pd.PeriodIndex(arr.asi8, dtype=arr.dtype).array
122-
result2 = period_array(arr.asi8, dtype=arr.dtype)
123-
result3 = PeriodArray._from_sequence(arr.asi8, dtype=arr.dtype)
124-
# FIXME: GH#64227 this goes through a different path and gives different behavior
125-
# result4 = PeriodArray._from_sequence(arr.asi8.astype(object), dtype=arr.dtype)
126-
result5 = PeriodArray._from_sequence(list(arr.asi8), dtype=arr.dtype)
119+
# During deprecation, integer inputs are still treated as year values
120+
expected_years = pd.PeriodIndex(
121+
[pd.Period(x, freq=arr.freq) for x in arr.asi8]
122+
).array
127123

128-
tm.assert_period_array_equal(result1, expected)
129-
tm.assert_period_array_equal(result2, expected)
130-
tm.assert_period_array_equal(result3, expected)
131-
# tm.assert_period_array_equal(result4, expected)
132-
tm.assert_period_array_equal(result5, expected)
124+
msg = "Passing integer-dtype data to PeriodArray/PeriodIndex is deprecated"
125+
with tm.assert_produces_warning(Pandas4Warning, match=msg):
126+
result1 = pd.PeriodIndex(arr.asi8, dtype=arr.dtype).array
127+
with tm.assert_produces_warning(Pandas4Warning, match=msg):
128+
result2 = period_array(arr.asi8, dtype=arr.dtype)
129+
with tm.assert_produces_warning(Pandas4Warning, match=msg):
130+
result3 = PeriodArray._from_sequence(arr.asi8, dtype=arr.dtype)
131+
with tm.assert_produces_warning(Pandas4Warning, match=msg):
132+
result5 = PeriodArray._from_sequence(list(arr.asi8), dtype=arr.dtype)
133+
134+
tm.assert_period_array_equal(result1, expected_years)
135+
tm.assert_period_array_equal(result2, expected_years)
136+
tm.assert_period_array_equal(result3, expected_years)
137+
tm.assert_period_array_equal(result5, expected_years)
138+
139+
# Object array path treats integers as ordinals (target behavior, no warning)
140+
result4 = PeriodArray._from_sequence(arr.asi8.astype(object), dtype=arr.dtype)
141+
tm.assert_period_array_equal(result4, arr)
133142

134143

135144
def test_from_td64nat_sequence_raises():

pandas/tests/base/test_conversion.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ def test_iter_box_period(self):
203203
"datetime64[ns, US/Central]",
204204
),
205205
(
206-
pd.PeriodIndex([2018, 2019], freq="Y"),
206+
pd.PeriodIndex(["2018", "2019"], freq="Y"),
207207
PeriodArray,
208208
pd.core.dtypes.dtypes.PeriodDtype("Y-DEC"),
209209
),

pandas/tests/indexes/multi/test_integrity.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,8 @@ def test_values_multiindex_datetimeindex():
8080
def test_values_multiindex_periodindex():
8181
# Test to ensure we hit the boxing / nobox part of MI.values
8282
ints = np.arange(2007, 2012)
83-
pidx = pd.PeriodIndex(ints, freq="D")
83+
strs = [str(x) for x in ints]
84+
pidx = pd.PeriodIndex(strs, freq="D")
8485

8586
idx = MultiIndex.from_arrays([ints, pidx])
8687
result = idx.values

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,19 @@
44

55

66
def test_is_full():
7-
index = PeriodIndex([2005, 2007, 2009], freq="Y")
7+
index = PeriodIndex(["2005", "2007", "2009"], freq="Y")
88
assert not index.is_full
99

10-
index = PeriodIndex([2005, 2006, 2007], freq="Y")
10+
index = PeriodIndex(["2005", "2006", "2007"], freq="Y")
1111
assert index.is_full
1212

13-
index = PeriodIndex([2005, 2005, 2007], freq="Y")
13+
index = PeriodIndex(["2005", "2005", "2007"], freq="Y")
1414
assert not index.is_full
1515

16-
index = PeriodIndex([2005, 2005, 2006], freq="Y")
16+
index = PeriodIndex(["2005", "2005", "2006"], freq="Y")
1717
assert index.is_full
1818

19-
index = PeriodIndex([2006, 2005, 2005], freq="Y")
19+
index = PeriodIndex(["2006", "2005", "2005"], freq="Y")
2020
with pytest.raises(ValueError, match="Index is not monotonic"):
2121
index.is_full
2222

pandas/tests/indexes/period/test_constructors.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -247,10 +247,13 @@ def test_constructor_fromarraylike(self):
247247
tm.assert_index_equal(PeriodIndex(list(idx.values)), idx)
248248

249249
msg = "freq not specified and cannot be inferred"
250-
with pytest.raises(ValueError, match=msg):
251-
PeriodIndex(idx.asi8)
252-
with pytest.raises(ValueError, match=msg):
253-
PeriodIndex(list(idx.asi8))
250+
depr_msg = "Passing integer-dtype data"
251+
with tm.assert_produces_warning(Pandas4Warning, match=depr_msg):
252+
with pytest.raises(ValueError, match=msg):
253+
PeriodIndex(idx.asi8)
254+
with tm.assert_produces_warning(Pandas4Warning, match=depr_msg):
255+
with pytest.raises(ValueError, match=msg):
256+
PeriodIndex(list(idx.asi8))
254257

255258
msg = "'Period' object is not iterable"
256259
with pytest.raises(TypeError, match=msg):
@@ -629,7 +632,7 @@ def test_recreate_from_data(self, freq):
629632
tm.assert_index_equal(idx, org)
630633

631634
def test_map_with_string_constructor(self):
632-
raw = [2005, 2007, 2009]
635+
raw = ["2005", "2007", "2009"]
633636
index = PeriodIndex(raw, freq="Y")
634637

635638
expected = Index([str(num) for num in raw])

pandas/tests/indexes/period/test_partial_slicing.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
class TestPeriodIndex:
1515
def test_getitem_periodindex_duplicates_string_slice(self):
1616
# monotonic
17-
idx = PeriodIndex([2000, 2007, 2007, 2009, 2009], freq="Y-JUN")
17+
idx = PeriodIndex(["2000", "2007", "2007", "2009", "2009"], freq="Y-JUN")
1818
ts = Series(np.random.default_rng(2).standard_normal(len(idx)), index=idx)
1919
original = ts.copy()
2020

@@ -25,7 +25,7 @@ def test_getitem_periodindex_duplicates_string_slice(self):
2525
tm.assert_series_equal(ts, original)
2626

2727
# not monotonic
28-
idx = PeriodIndex([2000, 2007, 2007, 2009, 2007], freq="Y-JUN")
28+
idx = PeriodIndex(["2000", "2007", "2007", "2009", "2007"], freq="Y-JUN")
2929
ts = Series(np.random.default_rng(2).standard_normal(len(idx)), index=idx)
3030

3131
result = ts["2007"]

pandas/tests/indexes/period/test_period.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,8 @@ def test_is_(self):
130130
assert not index.is_(index - 0)
131131

132132
def test_index_unique(self):
133-
idx = PeriodIndex([2000, 2007, 2007, 2009, 2009], freq="Y-JUN")
134-
expected = PeriodIndex([2000, 2007, 2009], freq="Y-JUN")
133+
idx = PeriodIndex(["2000", "2007", "2007", "2009", "2009"], freq="Y-JUN")
134+
expected = PeriodIndex(["2000", "2007", "2009"], freq="Y-JUN")
135135
tm.assert_index_equal(idx.unique(), expected)
136136
assert idx.nunique() == 3
137137

@@ -184,7 +184,7 @@ def test_with_multi_index(self):
184184
def test_map(self):
185185
# test_map_dictlike generally tests
186186

187-
index = PeriodIndex([2005, 2007, 2009], freq="Y")
187+
index = PeriodIndex(["2005", "2007", "2009"], freq="Y")
188188
result = index.map(lambda x: x.ordinal)
189189
exp = Index([x.ordinal for x in index])
190190
tm.assert_index_equal(result, exp)

pandas/tests/indexes/test_base.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1736,7 +1736,13 @@ def test_validate_1d_input(dtype):
17361736
*[[lambda x: Index(x, dtype=dtyp), {}] for dtyp in tm.ALL_REAL_NUMPY_DTYPES],
17371737
[DatetimeIndex, {}],
17381738
[TimedeltaIndex, {}],
1739-
[PeriodIndex, {"freq": "Y"}],
1739+
pytest.param(
1740+
PeriodIndex,
1741+
{"freq": "Y"},
1742+
marks=pytest.mark.filterwarnings(
1743+
"ignore:Passing integer-dtype data:pandas.errors.Pandas4Warning"
1744+
),
1745+
),
17401746
],
17411747
)
17421748
def test_construct_from_memoryview(klass, extra_kwargs):

0 commit comments

Comments
 (0)