diff --git a/pandas/core/indexing.py b/pandas/core/indexing.py index d3e75d43c6bd7..081f87078d9c9 100755 --- a/pandas/core/indexing.py +++ b/pandas/core/indexing.py @@ -1564,6 +1564,17 @@ def _convert_to_indexer(self, key, axis: int, is_setter: bool = False): # ------------------------------------------------------------------- def _setitem_with_indexer(self, indexer, value): + """ + _setitem_with_indexer is for setting values on a Series/DataFrame + using positional indexers. + + If the relevant keys are not present, the Series/DataFrame may be + expanded. + + This method is currently broken when dealing with non-unique Indexes, + since it goes from positional indexers back to labels when calling + BlockManager methods, see GH#12991, GH#22046, GH#15686. + """ # also has the side effect of consolidating in-place from pandas import Series @@ -1678,24 +1689,19 @@ def _setitem_with_indexer(self, indexer, value): info_idx = [info_idx] labels = item_labels[info_idx] - # if we have a partial multiindex, then need to adjust the plane - # indexer here - if len(labels) == 1 and isinstance( - self.obj[labels[0]].axes[0], ABCMultiIndex - ): + if len(labels) == 1: + # We can operate on a single column item = labels[0] - obj = self.obj[item] - index = obj.index - idx = indexer[:info_axis][0] + idx = indexer[0] - plane_indexer = tuple([idx]) + indexer[info_axis + 1 :] - lplane_indexer = length_of_indexer(plane_indexer[0], index) + plane_indexer = tuple([idx]) + lplane_indexer = length_of_indexer(plane_indexer[0], self.obj.index) # lplane_indexer gives the expected length of obj[idx] # require that we are setting the right number of values that # we are indexing - if is_list_like_indexer(value) and lplane_indexer != len(value): - + if is_list_like_indexer(value) and 0 != lplane_indexer != len(value): + # Exclude zero-len for e.g. boolean masking that is all-false raise ValueError( "cannot set using a multi-index " "selection indexer with a different " @@ -1704,12 +1710,11 @@ def _setitem_with_indexer(self, indexer, value): # non-mi else: - plane_indexer = indexer[:info_axis] + indexer[info_axis + 1 :] - plane_axis = self.obj.axes[:info_axis][0] - lplane_indexer = length_of_indexer(plane_indexer[0], plane_axis) + plane_indexer = indexer[:1] + lplane_indexer = length_of_indexer(plane_indexer[0], self.obj.index) def setter(item, v): - s = self.obj[item] + ser = self.obj[item] pi = plane_indexer[0] if lplane_indexer == 1 else plane_indexer # perform the equivalent of a setitem on the info axis @@ -1721,16 +1726,16 @@ def setter(item, v): com.is_null_slice(idx) or com.is_full_slice(idx, len(self.obj)) for idx in pi ): - s = v + ser = v else: # set the item, possibly having a dtype change - s._consolidate_inplace() - s = s.copy() - s._data = s._data.setitem(indexer=pi, value=v) - s._maybe_update_cacher(clear=True) + ser._consolidate_inplace() + ser = ser.copy() + ser._data = ser._data.setitem(indexer=pi, value=v) + ser._maybe_update_cacher(clear=True) # reset the sliced object if unique - self.obj[item] = s + self.obj[item] = ser # we need an iterable, with a ndim of at least 1 # eg. don't pass through np.array(0)