Skip to content

Commit 7b22448

Browse files
authored
DEPR: positional indexing on Series __getitem__/__setitem__ (#53201)
* DEPR: positional indexing on Series __getitem__/__setitem__ * troubleshoot docs * troubleshoot docs * update doc * update docs * docs * update fixture * update doctest
1 parent b968ce5 commit 7b22448

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+293
-176
lines changed

doc/source/user_guide/basics.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -1364,7 +1364,7 @@ We illustrate these fill methods on a simple Series:
13641364
13651365
rng = pd.date_range("1/3/2000", periods=8)
13661366
ts = pd.Series(np.random.randn(8), index=rng)
1367-
ts2 = ts[[0, 3, 6]]
1367+
ts2 = ts.iloc[[0, 3, 6]]
13681368
ts
13691369
ts2
13701370

doc/source/user_guide/cookbook.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -546,7 +546,7 @@ Unlike agg, apply's callable is passed a sub-DataFrame which gives you access to
546546
547547
def MyCust(x):
548548
if len(x) > 2:
549-
return x[1] * 1.234
549+
return x.iloc[1] * 1.234
550550
return pd.NaT
551551
552552
mhc = {"Mean": np.mean, "Max": np.max, "Custom": MyCust}

doc/source/user_guide/dsintro.rst

+5-5
Original file line numberDiff line numberDiff line change
@@ -102,15 +102,15 @@ However, operations such as slicing will also slice the index.
102102

103103
.. ipython:: python
104104
105-
s[0]
106-
s[:3]
105+
s.iloc[0]
106+
s.iloc[:3]
107107
s[s > s.median()]
108-
s[[4, 3, 1]]
108+
s.iloc[[4, 3, 1]]
109109
np.exp(s)
110110
111111
.. note::
112112

113-
We will address array-based indexing like ``s[[4, 3, 1]]``
113+
We will address array-based indexing like ``s.iloc[[4, 3, 1]]``
114114
in :ref:`section on indexing <indexing>`.
115115

116116
Like a NumPy array, a pandas :class:`Series` has a single :attr:`~Series.dtype`.
@@ -201,7 +201,7 @@ labels.
201201

202202
.. ipython:: python
203203
204-
s[1:] + s[:-1]
204+
s.iloc[1:] + s.iloc[:-1]
205205
206206
The result of an operation between unaligned :class:`Series` will have the **union** of
207207
the indexes involved. If a label is not found in one :class:`Series` or the other, the

doc/source/user_guide/missing_data.rst

+2-2
Original file line numberDiff line numberDiff line change
@@ -368,7 +368,7 @@ Index aware interpolation is available via the ``method`` keyword:
368368
.. ipython:: python
369369
:suppress:
370370
371-
ts2 = ts[[0, 1, 30, 60, 99]]
371+
ts2 = ts.iloc[[0, 1, 30, 60, 99]]
372372
373373
.. ipython:: python
374374
@@ -443,7 +443,7 @@ Compare several methods:
443443
444444
ser = pd.Series(np.arange(1, 10.1, 0.25) ** 2 + np.random.randn(37))
445445
missing = np.array([4, 13, 14, 15, 16, 17, 18, 20, 29])
446-
ser[missing] = np.nan
446+
ser.iloc[missing] = np.nan
447447
methods = ["linear", "quadratic", "cubic"]
448448
449449
df = pd.DataFrame({m: ser.interpolate(method=m) for m in methods})

doc/source/user_guide/timeseries.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -777,7 +777,7 @@ regularity will result in a ``DatetimeIndex``, although frequency is lost:
777777

778778
.. ipython:: python
779779
780-
ts2[[0, 2, 6]].index
780+
ts2.iloc[[0, 2, 6]].index
781781
782782
.. _timeseries.components:
783783

doc/source/whatsnew/v1.1.0.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ For example:
5555
pi = dti.to_period("D")
5656
ser_monotonic = pd.Series(np.arange(30), index=pi)
5757
shuffler = list(range(0, 30, 2)) + list(range(1, 31, 2))
58-
ser = ser_monotonic[shuffler]
58+
ser = ser_monotonic.iloc[shuffler]
5959
ser
6060
6161
.. ipython:: python

doc/source/whatsnew/v2.1.0.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,7 @@ Deprecations
264264
- Deprecated allowing ``downcast`` keyword other than ``None``, ``False``, "infer", or a dict with these as values in :meth:`Series.fillna`, :meth:`DataFrame.fillna` (:issue:`40988`)
265265
- Deprecated allowing arbitrary ``fill_value`` in :class:`SparseDtype`, in a future version the ``fill_value`` will need to be compatible with the ``dtype.subtype``, either a scalar that can be held by that subtype or ``NaN`` for integer or bool subtypes (:issue:`23124`)
266266
- Deprecated constructing :class:`SparseArray` from scalar data, pass a sequence instead (:issue:`53039`)
267-
-
267+
- Deprecated positional indexing on :class:`Series` with :meth:`Series.__getitem__` and :meth:`Series.__setitem__`, in a future version ``ser[item]`` will *always* interpret ``item`` as a label, not a position (:issue:`50617`)
268268

269269
.. ---------------------------------------------------------------------------
270270
.. _whatsnew_210.performance:

pandas/conftest.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -746,7 +746,7 @@ def series_with_multilevel_index() -> Series:
746746
index = MultiIndex.from_tuples(tuples)
747747
data = np.random.randn(8)
748748
ser = Series(data, index=index)
749-
ser[3] = np.NaN
749+
ser.iloc[3] = np.NaN
750750
return ser
751751

752752

pandas/core/apply.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1435,7 +1435,7 @@ def relabel_result(
14351435
com.get_callable_name(f) if not isinstance(f, str) else f for f in fun
14361436
]
14371437
col_idx_order = Index(s.index).get_indexer(fun)
1438-
s = s[col_idx_order]
1438+
s = s.iloc[col_idx_order]
14391439

14401440
# assign the new user-provided "named aggregation" as index names, and reindex
14411441
# it based on the whole user-provided names.

pandas/core/generic.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -6512,8 +6512,8 @@ def copy(self, deep: bool_t | None = True) -> Self:
65126512
Updates to the data shared by shallow copy and original is reflected
65136513
in both; deep copy remains unchanged.
65146514
6515-
>>> s[0] = 3
6516-
>>> shallow[1] = 4
6515+
>>> s.iloc[0] = 3
6516+
>>> shallow.iloc[1] = 4
65176517
>>> s
65186518
a 3
65196519
b 4

pandas/core/indexing.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1905,7 +1905,7 @@ def _setitem_with_indexer_split_path(self, indexer, value, name: str):
19051905
pass
19061906

19071907
elif self._is_scalar_access(indexer) and is_object_dtype(
1908-
self.obj.dtypes[ilocs[0]]
1908+
self.obj.dtypes._values[ilocs[0]]
19091909
):
19101910
# We are setting nested data, only possible for object dtype data
19111911
self._setitem_single_column(indexer[1], value, pi)

pandas/core/series.py

+36
Original file line numberDiff line numberDiff line change
@@ -973,6 +973,15 @@ def __getitem__(self, key):
973973
key = unpack_1tuple(key)
974974

975975
if is_integer(key) and self.index._should_fallback_to_positional:
976+
warnings.warn(
977+
# GH#50617
978+
"Series.__getitem__ treating keys as positions is deprecated. "
979+
"In a future version, integer keys will always be treated "
980+
"as labels (consistent with DataFrame behavior). To access "
981+
"a value by position, use `ser.iloc[pos]`",
982+
FutureWarning,
983+
stacklevel=find_stack_level(),
984+
)
976985
return self._values[key]
977986

978987
elif key_is_scalar:
@@ -1035,6 +1044,15 @@ def _get_with(self, key):
10351044
if not self.index._should_fallback_to_positional:
10361045
return self.loc[key]
10371046
else:
1047+
warnings.warn(
1048+
# GH#50617
1049+
"Series.__getitem__ treating keys as positions is deprecated. "
1050+
"In a future version, integer keys will always be treated "
1051+
"as labels (consistent with DataFrame behavior). To access "
1052+
"a value by position, use `ser.iloc[pos]`",
1053+
FutureWarning,
1054+
stacklevel=find_stack_level(),
1055+
)
10381056
return self.iloc[key]
10391057

10401058
# handle the dup indexing case GH#4246
@@ -1136,6 +1154,15 @@ def __setitem__(self, key, value) -> None:
11361154
# positional setter
11371155
# can't use _mgr.setitem_inplace yet bc could have *both*
11381156
# KeyError and then ValueError, xref GH#45070
1157+
warnings.warn(
1158+
# GH#50617
1159+
"Series.__setitem__ treating keys as positions is deprecated. "
1160+
"In a future version, integer keys will always be treated "
1161+
"as labels (consistent with DataFrame behavior). To set "
1162+
"a value by position, use `ser.iloc[pos] = value`",
1163+
FutureWarning,
1164+
stacklevel=find_stack_level(),
1165+
)
11391166
self._set_values(key, value)
11401167
else:
11411168
# GH#12862 adding a new key to the Series
@@ -1211,6 +1238,15 @@ def _set_with(self, key, value) -> None:
12111238
key_type = lib.infer_dtype(key, skipna=False)
12121239

12131240
if key_type == "integer":
1241+
warnings.warn(
1242+
# GH#50617
1243+
"Series.__setitem__ treating keys as positions is deprecated. "
1244+
"In a future version, integer keys will always be treated "
1245+
"as labels (consistent with DataFrame behavior). To set "
1246+
"a value by position, use `ser.iloc[pos] = value`",
1247+
FutureWarning,
1248+
stacklevel=find_stack_level(),
1249+
)
12141250
self._set_values(key, value)
12151251
else:
12161252
self._set_labels(key, value)

pandas/io/stata.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -2637,7 +2637,7 @@ def _prepare_pandas(self, data: DataFrame) -> None:
26372637
)
26382638
for key in self._convert_dates:
26392639
new_type = _convert_datetime_to_stata_type(self._convert_dates[key])
2640-
dtypes[key] = np.dtype(new_type)
2640+
dtypes.iloc[key] = np.dtype(new_type)
26412641

26422642
# Verify object arrays are strings and encode to bytes
26432643
self._encode_strings()

pandas/tests/apply/test_frame_apply.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1385,7 +1385,7 @@ def sum_div2(s):
13851385
def test_apply_getitem_axis_1():
13861386
# GH 13427
13871387
df = DataFrame({"a": [0, 1, 2], "b": [1, 2, 3]})
1388-
result = df[["a", "a"]].apply(lambda x: x[0] + x[1], axis=1)
1388+
result = df[["a", "a"]].apply(lambda x: x.iloc[0] + x.iloc[1], axis=1)
13891389
expected = Series([0, 2, 4])
13901390
tm.assert_series_equal(result, expected)
13911391

pandas/tests/copy_view/test_indexing.py

+11-1
Original file line numberDiff line numberDiff line change
@@ -802,7 +802,17 @@ def test_series_subset_set_with_indexer(
802802
s_orig = s.copy()
803803
subset = s[:]
804804

805-
indexer_si(subset)[indexer] = 0
805+
warn = None
806+
msg = "Series.__setitem__ treating keys as positions is deprecated"
807+
if (
808+
indexer_si is tm.setitem
809+
and isinstance(indexer, np.ndarray)
810+
and indexer.dtype.kind == "i"
811+
):
812+
warn = FutureWarning
813+
814+
with tm.assert_produces_warning(warn, match=msg):
815+
indexer_si(subset)[indexer] = 0
806816
expected = Series([0, 0, 3], index=["a", "b", "c"])
807817
tm.assert_series_equal(subset, expected)
808818

pandas/tests/copy_view/test_interp_fillna.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,7 @@ def test_fillna_ea_noop_shares_memory(
287287
if using_copy_on_write:
288288
assert np.shares_memory(get_array(df, "b"), get_array(df2, "b"))
289289
assert not df2._mgr._has_no_reference(1)
290-
elif isinstance(df.dtypes[0], ArrowDtype):
290+
elif isinstance(df.dtypes.iloc[0], ArrowDtype):
291291
# arrow is immutable, so no-ops do not need to copy underlying array
292292
assert np.shares_memory(get_array(df, "b"), get_array(df2, "b"))
293293
else:
@@ -317,7 +317,7 @@ def test_fillna_inplace_ea_noop_shares_memory(
317317
assert np.shares_memory(get_array(df, "b"), get_array(view, "b"))
318318
assert not df._mgr._has_no_reference(1)
319319
assert not view._mgr._has_no_reference(1)
320-
elif isinstance(df.dtypes[0], ArrowDtype):
320+
elif isinstance(df.dtypes.iloc[0], ArrowDtype):
321321
# arrow is immutable, so no-ops do not need to copy underlying array
322322
assert np.shares_memory(get_array(df, "b"), get_array(view, "b"))
323323
else:

pandas/tests/extension/base/getitem.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -330,9 +330,11 @@ def test_get(self, data):
330330
result = s.get("Z")
331331
assert result is None
332332

333-
assert s.get(4) == s.iloc[4]
334-
assert s.get(-1) == s.iloc[-1]
335-
assert s.get(len(s)) is None
333+
msg = "Series.__getitem__ treating keys as positions is deprecated"
334+
with tm.assert_produces_warning(FutureWarning, match=msg):
335+
assert s.get(4) == s.iloc[4]
336+
assert s.get(-1) == s.iloc[-1]
337+
assert s.get(len(s)) is None
336338

337339
# GH 21257
338340
s = pd.Series(data)

pandas/tests/frame/indexing/test_indexing.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -454,7 +454,7 @@ def test_setitem_corner(self, float_frame):
454454

455455
# set existing column
456456
dm["A"] = "bar"
457-
assert "bar" == dm["A"][0]
457+
assert "bar" == dm["A"].iloc[0]
458458

459459
dm = DataFrame(index=np.arange(3))
460460
dm["A"] = 1

pandas/tests/frame/methods/test_map.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def test_map(float_frame):
1919
float_frame.map(type)
2020

2121
# GH 465: function returning tuples
22-
result = float_frame.map(lambda x: (x, x))["A"][0]
22+
result = float_frame.map(lambda x: (x, x))["A"].iloc[0]
2323
assert isinstance(result, tuple)
2424

2525

pandas/tests/frame/methods/test_replace.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -614,8 +614,8 @@ def test_replace_mixed3(self):
614614
result = df.replace(3, df.mean().to_dict())
615615
expected = df.copy().astype("float64")
616616
m = df.mean()
617-
expected.iloc[0, 0] = m[0]
618-
expected.iloc[1, 1] = m[1]
617+
expected.iloc[0, 0] = m.iloc[0]
618+
expected.iloc[1, 1] = m.iloc[1]
619619
tm.assert_frame_equal(result, expected)
620620

621621
def test_replace_nullable_int_with_string_doesnt_cast(self):
@@ -1072,7 +1072,7 @@ def test_replace_period(self):
10721072
assert set(df.fname.values) == set(d["fname"].keys())
10731073

10741074
expected = DataFrame({"fname": [d["fname"][k] for k in df.fname.values]})
1075-
assert expected.dtypes[0] == "Period[M]"
1075+
assert expected.dtypes.iloc[0] == "Period[M]"
10761076
result = df.replace(d)
10771077
tm.assert_frame_equal(result, expected)
10781078

pandas/tests/frame/methods/test_values.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,9 @@ def test_values_mixed_dtypes(self, float_frame, float_string_frame):
3838
for j, value in enumerate(row):
3939
col = frame_cols[j]
4040
if np.isnan(value):
41-
assert np.isnan(frame[col][i])
41+
assert np.isnan(frame[col].iloc[i])
4242
else:
43-
assert value == frame[col][i]
43+
assert value == frame[col].iloc[i]
4444

4545
# mixed type
4646
arr = float_string_frame[["foo", "A"]].values

pandas/tests/frame/test_reductions.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -611,16 +611,16 @@ def test_operators_timedelta64(self):
611611

612612
# min
613613
result = diffs.min()
614-
assert result[0] == diffs.loc[0, "A"]
615-
assert result[1] == diffs.loc[0, "B"]
614+
assert result.iloc[0] == diffs.loc[0, "A"]
615+
assert result.iloc[1] == diffs.loc[0, "B"]
616616

617617
result = diffs.min(axis=1)
618618
assert (result == diffs.loc[0, "B"]).all()
619619

620620
# max
621621
result = diffs.max()
622-
assert result[0] == diffs.loc[2, "A"]
623-
assert result[1] == diffs.loc[2, "B"]
622+
assert result.iloc[0] == diffs.loc[2, "A"]
623+
assert result.iloc[1] == diffs.loc[2, "B"]
624624

625625
result = diffs.max(axis=1)
626626
assert (result == diffs["A"]).all()

pandas/tests/frame/test_stack_unstack.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1075,7 +1075,7 @@ def test_stack_full_multiIndex(self):
10751075
),
10761076
columns=Index(["B", "C"], name="Upper"),
10771077
)
1078-
expected["B"] = expected["B"].astype(df.dtypes[0])
1078+
expected["B"] = expected["B"].astype(df.dtypes.iloc[0])
10791079
tm.assert_frame_equal(result, expected)
10801080

10811081
@pytest.mark.parametrize("ordered", [False, True])

pandas/tests/groupby/test_groupby.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -1344,11 +1344,11 @@ def convert_force_pure(x):
13441344

13451345
result = grouped.agg(convert_fast)
13461346
assert result.dtype == np.object_
1347-
assert isinstance(result[0], Decimal)
1347+
assert isinstance(result.iloc[0], Decimal)
13481348

13491349
result = grouped.agg(convert_force_pure)
13501350
assert result.dtype == np.object_
1351-
assert isinstance(result[0], Decimal)
1351+
assert isinstance(result.iloc[0], Decimal)
13521352

13531353

13541354
def test_groupby_dtype_inference_empty():
@@ -1967,8 +1967,8 @@ def get_categorical_invalid_expected():
19671967
expected = DataFrame([], columns=[], index=idx)
19681968
return expected
19691969

1970-
is_per = isinstance(df.dtypes[0], pd.PeriodDtype)
1971-
is_dt64 = df.dtypes[0].kind == "M"
1970+
is_per = isinstance(df.dtypes.iloc[0], pd.PeriodDtype)
1971+
is_dt64 = df.dtypes.iloc[0].kind == "M"
19721972
is_cat = isinstance(values, Categorical)
19731973

19741974
if isinstance(values, Categorical) and not values.ordered and op in ["min", "max"]:

pandas/tests/indexes/datetimes/test_misc.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@ def test_datetimeindex_accessors(self):
5050
assert dti.dayofyear[0] == 1
5151
assert dti.dayofyear[120] == 121
5252

53-
assert dti.isocalendar().week[0] == 1
54-
assert dti.isocalendar().week[120] == 18
53+
assert dti.isocalendar().week.iloc[0] == 1
54+
assert dti.isocalendar().week.iloc[120] == 18
5555

5656
assert dti.quarter[0] == 1
5757
assert dti.quarter[120] == 2

pandas/tests/indexes/datetimes/test_partial_slicing.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -379,7 +379,7 @@ def test_partial_slice_requires_monotonicity(self):
379379
# Disallowed since 2.0 (GH 37819)
380380
ser = Series(np.arange(10), date_range("2014-01-01", periods=10))
381381

382-
nonmonotonic = ser[[3, 5, 4]]
382+
nonmonotonic = ser.iloc[[3, 5, 4]]
383383
timestamp = Timestamp("2014-01-10")
384384
with pytest.raises(
385385
KeyError, match="Value based partial slicing on non-monotonic"

0 commit comments

Comments
 (0)