Skip to content

Commit 3d259e6

Browse files
authored
Bug in iloc aligned objects (#37728)
1 parent 708b7b6 commit 3d259e6

File tree

5 files changed

+78
-35
lines changed

5 files changed

+78
-35
lines changed

doc/source/whatsnew/v1.2.0.rst

+1
Original file line numberDiff line numberDiff line change
@@ -608,6 +608,7 @@ Indexing
608608
- Bug in :meth:`DataFrame.xs` ignored ``droplevel=False`` for columns (:issue:`19056`)
609609
- Bug in :meth:`DataFrame.reindex` raising ``IndexingError`` wrongly for empty :class:`DataFrame` with ``tolerance`` not None or ``method="nearest"`` (:issue:`27315`)
610610
- Bug in indexing on a :class:`Series` or :class:`DataFrame` with a :class:`CategoricalIndex` using listlike indexer that contains elements that are in the index's ``categories`` but not in the index itself failing to raise ``KeyError`` (:issue:`37901`)
611+
- Bug in :meth:`DataFrame.iloc` and :meth:`Series.iloc` aligning objects in ``__setitem__`` (:issue:`22046`)
611612

612613
Missing
613614
^^^^^^^

pandas/core/indexing.py

+20-15
Original file line numberDiff line numberDiff line change
@@ -681,7 +681,7 @@ def __setitem__(self, key, value):
681681
self._has_valid_setitem_indexer(key)
682682

683683
iloc = self if self.name == "iloc" else self.obj.iloc
684-
iloc._setitem_with_indexer(indexer, value)
684+
iloc._setitem_with_indexer(indexer, value, self.name)
685685

686686
def _validate_key(self, key, axis: int):
687687
"""
@@ -1517,7 +1517,7 @@ def _get_setitem_indexer(self, key):
15171517

15181518
# -------------------------------------------------------------------
15191519

1520-
def _setitem_with_indexer(self, indexer, value):
1520+
def _setitem_with_indexer(self, indexer, value, name="iloc"):
15211521
"""
15221522
_setitem_with_indexer is for setting values on a Series/DataFrame
15231523
using positional indexers.
@@ -1593,7 +1593,7 @@ def _setitem_with_indexer(self, indexer, value):
15931593
new_indexer = convert_from_missing_indexer_tuple(
15941594
indexer, self.obj.axes
15951595
)
1596-
self._setitem_with_indexer(new_indexer, value)
1596+
self._setitem_with_indexer(new_indexer, value, name)
15971597

15981598
return
15991599

@@ -1624,11 +1624,11 @@ def _setitem_with_indexer(self, indexer, value):
16241624
# align and set the values
16251625
if take_split_path:
16261626
# We have to operate column-wise
1627-
self._setitem_with_indexer_split_path(indexer, value)
1627+
self._setitem_with_indexer_split_path(indexer, value, name)
16281628
else:
1629-
self._setitem_single_block(indexer, value)
1629+
self._setitem_single_block(indexer, value, name)
16301630

1631-
def _setitem_with_indexer_split_path(self, indexer, value):
1631+
def _setitem_with_indexer_split_path(self, indexer, value, name: str):
16321632
"""
16331633
Setitem column-wise.
16341634
"""
@@ -1642,7 +1642,7 @@ def _setitem_with_indexer_split_path(self, indexer, value):
16421642
if isinstance(indexer[0], np.ndarray) and indexer[0].ndim > 2:
16431643
raise ValueError(r"Cannot set values with ndim > 2")
16441644

1645-
if isinstance(value, ABCSeries):
1645+
if isinstance(value, ABCSeries) and name != "iloc":
16461646
value = self._align_series(indexer, value)
16471647

16481648
# Ensure we have something we can iterate over
@@ -1657,7 +1657,7 @@ def _setitem_with_indexer_split_path(self, indexer, value):
16571657
if is_list_like_indexer(value) and getattr(value, "ndim", 1) > 0:
16581658

16591659
if isinstance(value, ABCDataFrame):
1660-
self._setitem_with_indexer_frame_value(indexer, value)
1660+
self._setitem_with_indexer_frame_value(indexer, value, name)
16611661

16621662
elif np.ndim(value) == 2:
16631663
self._setitem_with_indexer_2d_value(indexer, value)
@@ -1714,7 +1714,7 @@ def _setitem_with_indexer_2d_value(self, indexer, value):
17141714
# setting with a list, re-coerces
17151715
self._setitem_single_column(loc, value[:, i].tolist(), pi)
17161716

1717-
def _setitem_with_indexer_frame_value(self, indexer, value: "DataFrame"):
1717+
def _setitem_with_indexer_frame_value(self, indexer, value: "DataFrame", name: str):
17181718
ilocs = self._ensure_iterable_column_indexer(indexer[1])
17191719

17201720
sub_indexer = list(indexer)
@@ -1724,7 +1724,13 @@ def _setitem_with_indexer_frame_value(self, indexer, value: "DataFrame"):
17241724

17251725
unique_cols = value.columns.is_unique
17261726

1727-
if not unique_cols and value.columns.equals(self.obj.columns):
1727+
# We do not want to align the value in case of iloc GH#37728
1728+
if name == "iloc":
1729+
for i, loc in enumerate(ilocs):
1730+
val = value.iloc[:, i]
1731+
self._setitem_single_column(loc, val, pi)
1732+
1733+
elif not unique_cols and value.columns.equals(self.obj.columns):
17281734
# We assume we are already aligned, see
17291735
# test_iloc_setitem_frame_duplicate_columns_multiple_blocks
17301736
for loc in ilocs:
@@ -1787,7 +1793,7 @@ def _setitem_single_column(self, loc: int, value, plane_indexer):
17871793
# reset the sliced object if unique
17881794
self.obj._iset_item(loc, ser)
17891795

1790-
def _setitem_single_block(self, indexer, value):
1796+
def _setitem_single_block(self, indexer, value, name: str):
17911797
"""
17921798
_setitem_with_indexer for the case when we have a single Block.
17931799
"""
@@ -1815,14 +1821,13 @@ def _setitem_single_block(self, indexer, value):
18151821
return
18161822

18171823
indexer = maybe_convert_ix(*indexer)
1818-
1819-
if isinstance(value, (ABCSeries, dict)):
1824+
if isinstance(value, ABCSeries) and name != "iloc" or isinstance(value, dict):
18201825
# TODO(EA): ExtensionBlock.setitem this causes issues with
18211826
# setting for extensionarrays that store dicts. Need to decide
18221827
# if it's worth supporting that.
18231828
value = self._align_series(indexer, Series(value))
18241829

1825-
elif isinstance(value, ABCDataFrame):
1830+
elif isinstance(value, ABCDataFrame) and name != "iloc":
18261831
value = self._align_frame(indexer, value)
18271832

18281833
# check for chained assignment
@@ -1854,7 +1859,7 @@ def _setitem_with_indexer_missing(self, indexer, value):
18541859
if index.is_unique:
18551860
new_indexer = index.get_indexer([new_index[-1]])
18561861
if (new_indexer != -1).any():
1857-
return self._setitem_with_indexer(new_indexer, value)
1862+
return self._setitem_with_indexer(new_indexer, value, "loc")
18581863

18591864
# this preserves dtype of the value
18601865
new_values = Series([value])._values

pandas/tests/frame/indexing/test_setitem.py

-9
Original file line numberDiff line numberDiff line change
@@ -289,15 +289,6 @@ def test_setitem_periodindex(self):
289289
assert isinstance(rs.index, PeriodIndex)
290290
tm.assert_index_equal(rs.index, rng)
291291

292-
@pytest.mark.parametrize("klass", [list, np.array])
293-
def test_iloc_setitem_bool_indexer(self, klass):
294-
# GH: 36741
295-
df = DataFrame({"flag": ["x", "y", "z"], "value": [1, 3, 4]})
296-
indexer = klass([True, False, False])
297-
df.iloc[indexer, 1] = df.iloc[indexer, 1] * 2
298-
expected = DataFrame({"flag": ["x", "y", "z"], "value": [2, 3, 4]})
299-
tm.assert_frame_equal(df, expected)
300-
301292

302293
class TestDataFrameSetItemSlicing:
303294
def test_setitem_slice_position(self):

pandas/tests/indexing/test_iloc.py

+41
Original file line numberDiff line numberDiff line change
@@ -801,6 +801,39 @@ def test_iloc_setitem_empty_frame_raises_with_3d_ndarray(self):
801801
with pytest.raises(ValueError, match=msg):
802802
obj.iloc[nd3] = 0
803803

804+
def test_iloc_assign_series_to_df_cell(self):
805+
# GH 37593
806+
df = DataFrame(columns=["a"], index=[0])
807+
df.iloc[0, 0] = Series([1, 2, 3])
808+
expected = DataFrame({"a": [Series([1, 2, 3])]}, columns=["a"], index=[0])
809+
tm.assert_frame_equal(df, expected)
810+
811+
@pytest.mark.parametrize("klass", [list, np.array])
812+
def test_iloc_setitem_bool_indexer(self, klass):
813+
# GH#36741
814+
df = DataFrame({"flag": ["x", "y", "z"], "value": [1, 3, 4]})
815+
indexer = klass([True, False, False])
816+
df.iloc[indexer, 1] = df.iloc[indexer, 1] * 2
817+
expected = DataFrame({"flag": ["x", "y", "z"], "value": [2, 3, 4]})
818+
tm.assert_frame_equal(df, expected)
819+
820+
@pytest.mark.parametrize("indexer", [[1], slice(1, 2)])
821+
def test_setitem_iloc_pure_position_based(self, indexer):
822+
# GH#22046
823+
df1 = DataFrame({"a2": [11, 12, 13], "b2": [14, 15, 16]})
824+
df2 = DataFrame({"a": [1, 2, 3], "b": [4, 5, 6], "c": [7, 8, 9]})
825+
df2.iloc[:, indexer] = df1.iloc[:, [0]]
826+
expected = DataFrame({"a": [1, 2, 3], "b": [11, 12, 13], "c": [7, 8, 9]})
827+
tm.assert_frame_equal(df2, expected)
828+
829+
def test_setitem_iloc_dictionary_value(self):
830+
# GH#37728
831+
df = DataFrame({"x": [1, 2], "y": [2, 2]})
832+
rhs = dict(x=9, y=99)
833+
df.iloc[1] = rhs
834+
expected = DataFrame({"x": [1, 9], "y": [2, 99]})
835+
tm.assert_frame_equal(df, expected)
836+
804837

805838
class TestILocErrors:
806839
# NB: this test should work for _any_ Series we can pass as
@@ -966,3 +999,11 @@ def test_iloc(self):
966999
def test_iloc_getitem_nonunique(self):
9671000
ser = Series([0, 1, 2], index=[0, 1, 0])
9681001
assert ser.iloc[2] == 2
1002+
1003+
def test_setitem_iloc_pure_position_based(self):
1004+
# GH#22046
1005+
ser1 = Series([1, 2, 3])
1006+
ser2 = Series([4, 5, 6], index=[1, 0, 2])
1007+
ser1.iloc[1:3] = ser2.iloc[1:3]
1008+
expected = Series([1, 5, 6])
1009+
tm.assert_series_equal(ser1, expected)

pandas/tests/indexing/test_indexing.py

+16-11
Original file line numberDiff line numberDiff line change
@@ -668,43 +668,48 @@ def test_float_index_at_iat(self):
668668
def test_rhs_alignment(self):
669669
# GH8258, tests that both rows & columns are aligned to what is
670670
# assigned to. covers both uniform data-type & multi-type cases
671-
def run_tests(df, rhs, right):
671+
def run_tests(df, rhs, right_loc, right_iloc):
672672
# label, index, slice
673673
lbl_one, idx_one, slice_one = list("bcd"), [1, 2, 3], slice(1, 4)
674674
lbl_two, idx_two, slice_two = ["joe", "jolie"], [1, 2], slice(1, 3)
675675

676676
left = df.copy()
677677
left.loc[lbl_one, lbl_two] = rhs
678-
tm.assert_frame_equal(left, right)
678+
tm.assert_frame_equal(left, right_loc)
679679

680680
left = df.copy()
681681
left.iloc[idx_one, idx_two] = rhs
682-
tm.assert_frame_equal(left, right)
682+
tm.assert_frame_equal(left, right_iloc)
683683

684684
left = df.copy()
685685
left.iloc[slice_one, slice_two] = rhs
686-
tm.assert_frame_equal(left, right)
686+
tm.assert_frame_equal(left, right_iloc)
687687

688688
xs = np.arange(20).reshape(5, 4)
689689
cols = ["jim", "joe", "jolie", "joline"]
690-
df = DataFrame(xs, columns=cols, index=list("abcde"))
690+
df = DataFrame(xs, columns=cols, index=list("abcde"), dtype="int64")
691691

692692
# right hand side; permute the indices and multiplpy by -2
693693
rhs = -2 * df.iloc[3:0:-1, 2:0:-1]
694694

695695
# expected `right` result; just multiply by -2
696-
right = df.copy()
697-
right.iloc[1:4, 1:3] *= -2
696+
right_iloc = df.copy()
697+
right_iloc["joe"] = [1, 14, 10, 6, 17]
698+
right_iloc["jolie"] = [2, 13, 9, 5, 18]
699+
right_iloc.iloc[1:4, 1:3] *= -2
700+
right_loc = df.copy()
701+
right_loc.iloc[1:4, 1:3] *= -2
698702

699703
# run tests with uniform dtypes
700-
run_tests(df, rhs, right)
704+
run_tests(df, rhs, right_loc, right_iloc)
701705

702706
# make frames multi-type & re-run tests
703-
for frame in [df, rhs, right]:
707+
for frame in [df, rhs, right_loc, right_iloc]:
704708
frame["joe"] = frame["joe"].astype("float64")
705709
frame["jolie"] = frame["jolie"].map("@{}".format)
706-
707-
run_tests(df, rhs, right)
710+
right_iloc["joe"] = [1.0, "@-28", "@-20", "@-12", 17.0]
711+
right_iloc["jolie"] = ["@2", -26.0, -18.0, -10.0, "@18"]
712+
run_tests(df, rhs, right_loc, right_iloc)
708713

709714
def test_str_label_slicing_with_negative_step(self):
710715
SLC = pd.IndexSlice

0 commit comments

Comments
 (0)