From a076e6a5a8d27f828f401ad1c0a0deb443847f55 Mon Sep 17 00:00:00 2001 From: jbrockmendel Date: Fri, 7 Feb 2020 09:59:02 -0800 Subject: [PATCH 01/11] REF: remove iloc case from _convert_slice_indexer --- pandas/core/indexes/base.py | 20 +++++++++++--------- pandas/core/indexes/numeric.py | 5 ++--- pandas/core/indexing.py | 9 +++++---- pandas/core/series.py | 7 +++++-- 4 files changed, 23 insertions(+), 18 deletions(-) diff --git a/pandas/core/indexes/base.py b/pandas/core/indexes/base.py index e431d0bcf7e9b..38af0f7afee44 100644 --- a/pandas/core/indexes/base.py +++ b/pandas/core/indexes/base.py @@ -3142,6 +3142,15 @@ def _convert_scalar_indexer(self, key, kind: str_t): return key + def _validate_positional_slice(self, key: slice): + """ + For positional indexing, a slice must have either int or None + for each of start, stop, and step. + """ + self._validate_indexer("slice", key.start, "iloc") + self._validate_indexer("slice", key.stop, "iloc") + self._validate_indexer("slice", key.step, "iloc") + def _convert_slice_indexer(self, key: slice, kind=None): """ Convert a slice indexer. @@ -3152,16 +3161,9 @@ def _convert_slice_indexer(self, key: slice, kind=None): Parameters ---------- key : label of the slice bound - kind : {'loc', 'getitem', 'iloc'} or None + kind : {'loc', 'getitem'} or None """ - assert kind in ["loc", "getitem", "iloc", None] - - # validate iloc - if kind == "iloc": - self._validate_indexer("slice", key.start, "iloc") - self._validate_indexer("slice", key.stop, "iloc") - self._validate_indexer("slice", key.step, "iloc") - return key + assert kind in ["loc", "getitem", None], kind # potentially cast the bounds to integers start, stop, step = key.start, key.stop, key.step diff --git a/pandas/core/indexes/numeric.py b/pandas/core/indexes/numeric.py index d67c40a78d807..f09713409c6cf 100644 --- a/pandas/core/indexes/numeric.py +++ b/pandas/core/indexes/numeric.py @@ -394,10 +394,9 @@ def _convert_scalar_indexer(self, key, kind: str): @Appender(Index._convert_slice_indexer.__doc__) def _convert_slice_indexer(self, key: slice, kind=None): + assert kind in ["loc", "getitem", None] - if kind == "iloc": - return super()._convert_slice_indexer(key, kind=kind) - + # We always treat __getitem__ slicing as label-based # translate to locations return self.slice_indexer(key.start, key.stop, key.step, kind=kind) diff --git a/pandas/core/indexing.py b/pandas/core/indexing.py index bf42cf0330ef0..453154b63f547 100755 --- a/pandas/core/indexing.py +++ b/pandas/core/indexing.py @@ -1752,7 +1752,7 @@ def _get_slice_axis(self, slice_obj: slice, axis: int): labels = obj._get_axis(axis) indexer = labels.slice_indexer( - slice_obj.start, slice_obj.stop, slice_obj.step, kind=self.name + slice_obj.start, slice_obj.stop, slice_obj.step, kind="loc" ) if isinstance(indexer, slice): @@ -2019,8 +2019,8 @@ def _get_slice_axis(self, slice_obj: slice, axis: int): return obj.copy(deep=False) labels = obj._get_axis(axis) - indexer = labels._convert_slice_indexer(slice_obj, kind="iloc") - return self.obj._slice(indexer, axis=axis, kind="iloc") + labels._validate_positional_slice(slice_obj) + return self.obj._slice(slice_obj, axis=axis, kind="iloc") def _convert_to_indexer(self, key, axis: int, is_setter: bool = False): """ @@ -2030,7 +2030,8 @@ def _convert_to_indexer(self, key, axis: int, is_setter: bool = False): # make need to convert a float key if isinstance(key, slice): - return labels._convert_slice_indexer(key, kind="iloc") + labels._validate_positional_slice(key) + return key elif is_float(key): labels._validate_indexer("positional", key, "iloc") diff --git a/pandas/core/series.py b/pandas/core/series.py index 0786674daf874..05c61c014c08e 100644 --- a/pandas/core/series.py +++ b/pandas/core/series.py @@ -842,7 +842,10 @@ def _ixs(self, i: int, axis: int = 0): def _slice(self, slobj: slice, axis: int = 0, kind: str = "getitem") -> "Series": assert kind in ["getitem", "iloc"] - slobj = self.index._convert_slice_indexer(slobj, kind=kind) + if kind == "getitem": + # If called from getitem, we need to determine whether + # this slice is positional or label-based. + slobj = self.index._convert_slice_indexer(slobj, kind="getitem") return self._get_values(slobj) def __getitem__(self, key): @@ -884,7 +887,7 @@ def __getitem__(self, key): def _get_with(self, key): # other: fancy integer or otherwise if isinstance(key, slice): - return self._slice(key) + return self._slice(key, kind="getitem") elif isinstance(key, ABCDataFrame): raise TypeError( "Indexing a Series with DataFrame is not " From ccd7ad8d40ac10d0c0d0fd0212741412181342fc Mon Sep 17 00:00:00 2001 From: jbrockmendel Date: Fri, 7 Feb 2020 17:41:38 -0800 Subject: [PATCH 02/11] REF: Move Loc-only methods to Loc (#31589) * REF: Move Loc-only methods to Loc * rebase fixup * merge fixup --- pandas/core/indexing.py | 392 +++++++++++++++++++++------------------- 1 file changed, 204 insertions(+), 188 deletions(-) diff --git a/pandas/core/indexing.py b/pandas/core/indexing.py index 453154b63f547..70092c70a76ad 100755 --- a/pandas/core/indexing.py +++ b/pandas/core/indexing.py @@ -696,40 +696,6 @@ def _convert_tuple(self, key, is_setter: bool = False): keyidx.append(idx) return tuple(keyidx) - def _has_valid_setitem_indexer(self, indexer) -> bool: - return True - - def _has_valid_positional_setitem_indexer(self, indexer) -> bool: - """ - Validate that a positional indexer cannot enlarge its target - will raise if needed, does not modify the indexer externally. - - Returns - ------- - bool - """ - if isinstance(indexer, dict): - raise IndexError(f"{self.name} cannot enlarge its target object") - else: - if not isinstance(indexer, tuple): - indexer = _tuplify(self.ndim, indexer) - for ax, i in zip(self.obj.axes, indexer): - if isinstance(i, slice): - # should check the stop slice? - pass - elif is_list_like_indexer(i): - # should check the elements? - pass - elif is_integer(i): - if i >= len(ax): - raise IndexError( - f"{self.name} cannot enlarge its target object" - ) - elif isinstance(i, dict): - raise IndexError(f"{self.name} cannot enlarge its target object") - - return True - def _setitem_with_indexer(self, indexer, value): self._has_valid_setitem_indexer(indexer) @@ -1218,80 +1184,6 @@ def _align_frame(self, indexer, df: ABCDataFrame): raise ValueError("Incompatible indexer with DataFrame") - def _getitem_tuple(self, tup: Tuple): - try: - return self._getitem_lowerdim(tup) - except IndexingError: - pass - - # no multi-index, so validate all of the indexers - self._has_valid_tuple(tup) - - # ugly hack for GH #836 - if self._multi_take_opportunity(tup): - return self._multi_take(tup) - - # no shortcut needed - retval = self.obj - for i, key in enumerate(tup): - if com.is_null_slice(key): - continue - - retval = getattr(retval, self.name)._getitem_axis(key, axis=i) - - return retval - - def _multi_take_opportunity(self, tup: Tuple) -> bool: - """ - Check whether there is the possibility to use ``_multi_take``. - - Currently the limit is that all axes being indexed, must be indexed with - list-likes. - - Parameters - ---------- - tup : tuple - Tuple of indexers, one per axis. - - Returns - ------- - bool - Whether the current indexing, - can be passed through `_multi_take`. - """ - if not all(is_list_like_indexer(x) for x in tup): - return False - - # just too complicated - if any(com.is_bool_indexer(x) for x in tup): - return False - - return True - - def _multi_take(self, tup: Tuple): - """ - Create the indexers for the passed tuple of keys, and - executes the take operation. This allows the take operation to be - executed all at once, rather than once for each dimension. - Improving efficiency. - - Parameters - ---------- - tup : tuple - Tuple of indexers, one per axis. - - Returns - ------- - values: same type as the object being indexed - """ - # GH 836 - o = self.obj - d = { - axis: self._get_listlike_indexer(key, axis) - for (key, axis) in zip(tup, o._AXIS_ORDERS) - } - return o._reindex_with_indexers(d, copy=True, allow_dups=True) - def _handle_lowerdim_multi_index_axis0(self, tup: Tuple): # we have an axis0 multi-index, handle or raise axis = self.axis or 0 @@ -1412,86 +1304,6 @@ def _getitem_nested_tuple(self, tup: Tuple): return obj - def _get_listlike_indexer(self, key, axis: int, raise_missing: bool = False): - """ - Transform a list-like of keys into a new index and an indexer. - - Parameters - ---------- - key : list-like - Targeted labels. - axis: int - Dimension on which the indexing is being made. - raise_missing: bool, default False - Whether to raise a KeyError if some labels were not found. - Will be removed in the future, and then this method will always behave as - if ``raise_missing=True``. - - Raises - ------ - KeyError - If at least one key was requested but none was found, and - raise_missing=True. - - Returns - ------- - keyarr: Index - New index (coinciding with 'key' if the axis is unique). - values : array-like - Indexer for the return object, -1 denotes keys not found. - """ - o = self.obj - ax = o._get_axis(axis) - - # Have the index compute an indexer or return None - # if it cannot handle: - assert self.name == "loc" - indexer, keyarr = ax._convert_listlike_indexer(key) - # We only act on all found values: - if indexer is not None and (indexer != -1).all(): - self._validate_read_indexer(key, indexer, axis, raise_missing=raise_missing) - return ax[indexer], indexer - - if ax.is_unique and not getattr(ax, "is_overlapping", False): - indexer = ax.get_indexer_for(key) - keyarr = ax.reindex(keyarr)[0] - else: - keyarr, indexer, new_indexer = ax._reindex_non_unique(keyarr) - - self._validate_read_indexer(keyarr, indexer, axis, raise_missing=raise_missing) - return keyarr, indexer - - def _getitem_iterable(self, key, axis: int): - """ - Index current object with an an iterable collection of keys. - - Parameters - ---------- - key : iterable - Targeted labels. - axis: int - Dimension on which the indexing is being made. - - Raises - ------ - KeyError - If no key was found. Will change in the future to raise if not all - keys were found. - - Returns - ------- - scalar, DataFrame, or Series: indexed value(s). - """ - # we assume that not com.is_bool_indexer(key), as that is - # handled before we get here. - self._validate_key(key, axis) - - # A collection of keys - keyarr, indexer = self._get_listlike_indexer(key, axis, raise_missing=False) - return self.obj._reindex_with_indexers( - {axis: [keyarr, indexer]}, copy=True, allow_dups=True - ) - def _validate_read_indexer( self, key, indexer, axis: int, raise_missing: bool = False ): @@ -1575,9 +1387,15 @@ def __getitem__(self, key): def _is_scalar_access(self, key: Tuple): raise NotImplementedError() + def _getitem_tuple(self, tup: Tuple): + raise AbstractMethodError(self) + def _getitem_axis(self, key, axis: int): raise NotImplementedError() + def _has_valid_setitem_indexer(self, indexer) -> bool: + raise AbstractMethodError(self) + def _getbool_axis(self, key, axis: int): # caller is responsible for ensuring non-None axis labels = self.obj._get_axis(axis) @@ -1595,6 +1413,9 @@ class _LocIndexer(_LocationIndexer): "index is integers), listlike of labels, boolean" ) + # ------------------------------------------------------------------- + # Key Checks + @Appender(_LocationIndexer._validate_key.__doc__) def _validate_key(self, key, axis: int): @@ -1613,6 +1434,9 @@ def _validate_key(self, key, axis: int): labels = self.obj._get_axis(axis) labels._convert_scalar_indexer(key, kind="loc") + def _has_valid_setitem_indexer(self, indexer) -> bool: + return True + def _is_scalar_access(self, key: Tuple) -> bool: """ Returns @@ -1644,6 +1468,61 @@ def _is_scalar_access(self, key: Tuple) -> bool: return True + # ------------------------------------------------------------------- + # MultiIndex Handling + + def _multi_take_opportunity(self, tup: Tuple) -> bool: + """ + Check whether there is the possibility to use ``_multi_take``. + + Currently the limit is that all axes being indexed, must be indexed with + list-likes. + + Parameters + ---------- + tup : tuple + Tuple of indexers, one per axis. + + Returns + ------- + bool + Whether the current indexing, + can be passed through `_multi_take`. + """ + if not all(is_list_like_indexer(x) for x in tup): + return False + + # just too complicated + if any(com.is_bool_indexer(x) for x in tup): + return False + + return True + + def _multi_take(self, tup: Tuple): + """ + Create the indexers for the passed tuple of keys, and + executes the take operation. This allows the take operation to be + executed all at once, rather than once for each dimension. + Improving efficiency. + + Parameters + ---------- + tup : tuple + Tuple of indexers, one per axis. + + Returns + ------- + values: same type as the object being indexed + """ + # GH 836 + d = { + axis: self._get_listlike_indexer(key, axis) + for (key, axis) in zip(tup, self.obj._AXIS_ORDERS) + } + return self.obj._reindex_with_indexers(d, copy=True, allow_dups=True) + + # ------------------------------------------------------------------- + def _get_partial_string_timestamp_match_key(self, key, labels): """ Translate any partial string timestamp matches in key, returning the @@ -1676,6 +1555,60 @@ def _get_partial_string_timestamp_match_key(self, key, labels): return key + def _getitem_iterable(self, key, axis: int): + """ + Index current object with an an iterable collection of keys. + + Parameters + ---------- + key : iterable + Targeted labels. + axis: int + Dimension on which the indexing is being made. + + Raises + ------ + KeyError + If no key was found. Will change in the future to raise if not all + keys were found. + + Returns + ------- + scalar, DataFrame, or Series: indexed value(s). + """ + # we assume that not com.is_bool_indexer(key), as that is + # handled before we get here. + self._validate_key(key, axis) + + # A collection of keys + keyarr, indexer = self._get_listlike_indexer(key, axis, raise_missing=False) + return self.obj._reindex_with_indexers( + {axis: [keyarr, indexer]}, copy=True, allow_dups=True + ) + + def _getitem_tuple(self, tup: Tuple): + try: + return self._getitem_lowerdim(tup) + except IndexingError: + pass + + # no multi-index, so validate all of the indexers + self._has_valid_tuple(tup) + + # ugly hack for GH #836 + if self._multi_take_opportunity(tup): + return self._multi_take(tup) + + # no shortcut needed + retval = self.obj + for i, key in enumerate(tup): + if com.is_null_slice(key): + continue + + retval = getattr(retval, self.name)._getitem_axis(key, axis=i) + + return retval + def _getitem_axis(self, key, axis: int): key = item_from_zerodim(key) if is_iterator(key): @@ -1842,6 +1775,53 @@ def _convert_to_indexer(self, key, axis: int, is_setter: bool = False): return {"key": key} raise + def _get_listlike_indexer(self, key, axis: int, raise_missing: bool = False): + """ + Transform a list-like of keys into a new index and an indexer. + + Parameters + ---------- + key : list-like + Targeted labels. + axis: int + Dimension on which the indexing is being made. + raise_missing: bool, default False + Whether to raise a KeyError if some labels were not found. + Will be removed in the future, and then this method will always behave as + if ``raise_missing=True``. + + Raises + ------ + KeyError + If at least one key was requested but none was found, and + raise_missing=True. + + Returns + ------- + keyarr: Index + New index (coinciding with 'key' if the axis is unique). + values : array-like + Indexer for the return object, -1 denotes keys not found. + """ + ax = self.obj._get_axis(axis) + + # Have the index compute an indexer or return None + # if it cannot handle: + indexer, keyarr = ax._convert_listlike_indexer(key) + # We only act on all found values: + if indexer is not None and (indexer != -1).all(): + self._validate_read_indexer(key, indexer, axis, raise_missing=raise_missing) + return ax[indexer], indexer + + if ax.is_unique and not getattr(ax, "is_overlapping", False): + indexer = ax.get_indexer_for(key) + keyarr = ax.reindex(keyarr)[0] + else: + keyarr, indexer, new_indexer = ax._reindex_non_unique(keyarr) + + self._validate_read_indexer(keyarr, indexer, axis, raise_missing=raise_missing) + return keyarr, indexer + @Appender(IndexingMixin.iloc.__doc__) class _iLocIndexer(_LocationIndexer): @@ -1851,6 +1831,9 @@ class _iLocIndexer(_LocationIndexer): ) _takeable = True + # ------------------------------------------------------------------- + # Key Checks + def _validate_key(self, key, axis: int): if com.is_bool_indexer(key): if hasattr(key, "index") and isinstance(key.index, Index): @@ -1891,6 +1874,37 @@ def _validate_key(self, key, axis: int): def _has_valid_setitem_indexer(self, indexer): self._has_valid_positional_setitem_indexer(indexer) + def _has_valid_positional_setitem_indexer(self, indexer) -> bool: + """ + Validate that a positional indexer cannot enlarge its target + will raise if needed, does not modify the indexer externally. + + Returns + ------- + bool + """ + if isinstance(indexer, dict): + raise IndexError(f"{self.name} cannot enlarge its target object") + else: + if not isinstance(indexer, tuple): + indexer = _tuplify(self.ndim, indexer) + for ax, i in zip(self.obj.axes, indexer): + if isinstance(i, slice): + # should check the stop slice? + pass + elif is_list_like_indexer(i): + # should check the elements? + pass + elif is_integer(i): + if i >= len(ax): + raise IndexError( + f"{self.name} cannot enlarge its target object" + ) + elif isinstance(i, dict): + raise IndexError(f"{self.name} cannot enlarge its target object") + + return True + def _is_scalar_access(self, key: Tuple) -> bool: """ Returns @@ -1934,6 +1948,8 @@ def _validate_integer(self, key: int, axis: int) -> None: if key >= len_axis or key < -len_axis: raise IndexError("single positional indexer is out-of-bounds") + # ------------------------------------------------------------------- + def _getitem_tuple(self, tup: Tuple): self._has_valid_tuple(tup) From 6b7ba980fafab42f08e51cb6c13a542fd843f446 Mon Sep 17 00:00:00 2001 From: jbrockmendel Date: Sun, 9 Feb 2020 11:17:55 -0800 Subject: [PATCH 03/11] Fix incorrect internal usage of loc, xref #31810 --- pandas/tests/frame/conftest.py | 8 ++++---- pandas/tests/frame/test_analytics.py | 8 ++++---- pandas/tests/frame/test_cumulative.py | 24 ++++++++++++------------ pandas/tests/frame/test_to_csv.py | 2 +- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/pandas/tests/frame/conftest.py b/pandas/tests/frame/conftest.py index 774eb443c45fe..939a17db56213 100644 --- a/pandas/tests/frame/conftest.py +++ b/pandas/tests/frame/conftest.py @@ -33,8 +33,8 @@ def float_frame_with_na(): """ df = DataFrame(tm.getSeriesData()) # set some NAs - df.loc[5:10] = np.nan - df.loc[15:20, -2:] = np.nan + df.iloc[5:10] = np.nan + df.iloc[15:20, -2:] = np.nan return df @@ -67,8 +67,8 @@ def bool_frame_with_na(): df = DataFrame(tm.getSeriesData()) > 0 df = df.astype(object) # set some NAs - df.loc[5:10] = np.nan - df.loc[15:20, -2:] = np.nan + df.iloc[5:10] = np.nan + df.iloc[15:20, -2:] = np.nan return df diff --git a/pandas/tests/frame/test_analytics.py b/pandas/tests/frame/test_analytics.py index 25b2997eb088f..f2bc0711a4b2e 100644 --- a/pandas/tests/frame/test_analytics.py +++ b/pandas/tests/frame/test_analytics.py @@ -908,8 +908,8 @@ def test_sum_bools(self): def test_idxmin(self, float_frame, int_frame): frame = float_frame - frame.loc[5:10] = np.nan - frame.loc[15:20, -2:] = np.nan + frame.iloc[5:10] = np.nan + frame.iloc[15:20, -2:] = np.nan for skipna in [True, False]: for axis in [0, 1]: for df in [frame, int_frame]: @@ -923,8 +923,8 @@ def test_idxmin(self, float_frame, int_frame): def test_idxmax(self, float_frame, int_frame): frame = float_frame - frame.loc[5:10] = np.nan - frame.loc[15:20, -2:] = np.nan + frame.iloc[5:10] = np.nan + frame.iloc[15:20, -2:] = np.nan for skipna in [True, False]: for axis in [0, 1]: for df in [frame, int_frame]: diff --git a/pandas/tests/frame/test_cumulative.py b/pandas/tests/frame/test_cumulative.py index b545d6aa8afd3..248f3500c41df 100644 --- a/pandas/tests/frame/test_cumulative.py +++ b/pandas/tests/frame/test_cumulative.py @@ -22,9 +22,9 @@ def test_cumsum_corner(self): result = dm.cumsum() # noqa def test_cumsum(self, datetime_frame): - datetime_frame.loc[5:10, 0] = np.nan - datetime_frame.loc[10:15, 1] = np.nan - datetime_frame.loc[15:, 2] = np.nan + datetime_frame.iloc[5:10, 0] = np.nan + datetime_frame.iloc[10:15, 1] = np.nan + datetime_frame.iloc[15:, 2] = np.nan # axis = 0 cumsum = datetime_frame.cumsum() @@ -45,9 +45,9 @@ def test_cumsum(self, datetime_frame): assert np.shape(cumsum_xs) == np.shape(datetime_frame) def test_cumprod(self, datetime_frame): - datetime_frame.loc[5:10, 0] = np.nan - datetime_frame.loc[10:15, 1] = np.nan - datetime_frame.loc[15:, 2] = np.nan + datetime_frame.iloc[5:10, 0] = np.nan + datetime_frame.iloc[10:15, 1] = np.nan + datetime_frame.iloc[15:, 2] = np.nan # axis = 0 cumprod = datetime_frame.cumprod() @@ -74,9 +74,9 @@ def test_cumprod(self, datetime_frame): df.cumprod(1) def test_cummin(self, datetime_frame): - datetime_frame.loc[5:10, 0] = np.nan - datetime_frame.loc[10:15, 1] = np.nan - datetime_frame.loc[15:, 2] = np.nan + datetime_frame.iloc[5:10, 0] = np.nan + datetime_frame.iloc[10:15, 1] = np.nan + datetime_frame.iloc[15:, 2] = np.nan # axis = 0 cummin = datetime_frame.cummin() @@ -97,9 +97,9 @@ def test_cummin(self, datetime_frame): assert np.shape(cummin_xs) == np.shape(datetime_frame) def test_cummax(self, datetime_frame): - datetime_frame.loc[5:10, 0] = np.nan - datetime_frame.loc[10:15, 1] = np.nan - datetime_frame.loc[15:, 2] = np.nan + datetime_frame.iloc[5:10, 0] = np.nan + datetime_frame.iloc[10:15, 1] = np.nan + datetime_frame.iloc[15:, 2] = np.nan # axis = 0 cummax = datetime_frame.cummax() diff --git a/pandas/tests/frame/test_to_csv.py b/pandas/tests/frame/test_to_csv.py index aeff92971b42a..2e68ae6b21feb 100644 --- a/pandas/tests/frame/test_to_csv.py +++ b/pandas/tests/frame/test_to_csv.py @@ -761,7 +761,7 @@ def create_cols(name): ) # add in some nans - df_float.loc[30:50, 1:3] = np.nan + df_float.iloc[30:50, 1:3] = np.nan # ## this is a bug in read_csv right now #### # df_dt.loc[30:50,1:3] = np.nan From 1970398498d0ff90ff7724ede5339ce947b29b9c Mon Sep 17 00:00:00 2001 From: jbrockmendel Date: Sun, 9 Feb 2020 18:40:41 -0800 Subject: [PATCH 04/11] BUG: loc.__setitem__ incorrectly accepting positional slices --- pandas/core/indexes/base.py | 4 +++ pandas/tests/frame/indexing/test_indexing.py | 10 +++--- pandas/tests/frame/methods/test_asof.py | 6 ++-- pandas/tests/frame/test_block_internals.py | 2 +- pandas/tests/indexing/test_indexing.py | 30 +++++++++++++++++ pandas/tests/io/pytables/test_store.py | 34 ++++++++++---------- 6 files changed, 60 insertions(+), 26 deletions(-) diff --git a/pandas/core/indexes/base.py b/pandas/core/indexes/base.py index 6bc15a5f89e2a..511977c44250f 100644 --- a/pandas/core/indexes/base.py +++ b/pandas/core/indexes/base.py @@ -3206,8 +3206,12 @@ def is_int(v): raise if is_null_slicer: + # It doesn't matter if we are positional or label based indexer = key elif is_positional: + if kind == "loc": + # GH#16121, GH#24612, GH#31810 + self._invalid_indexer("slice", key) indexer = key else: indexer = self.slice_indexer(start, stop, step, kind=kind) diff --git a/pandas/tests/frame/indexing/test_indexing.py b/pandas/tests/frame/indexing/test_indexing.py index 6700d9c261791..b44dc7652ba89 100644 --- a/pandas/tests/frame/indexing/test_indexing.py +++ b/pandas/tests/frame/indexing/test_indexing.py @@ -1203,7 +1203,7 @@ def test_setitem_frame_mixed(self, float_string_frame): piece = DataFrame( [[1.0, 2.0], [3.0, 4.0]], index=f.index[0:2], columns=["A", "B"] ) - key = (slice(None, 2), ["A", "B"]) + key = (f.index[slice(None, 2)], ["A", "B"]) f.loc[key] = piece tm.assert_almost_equal(f.loc[f.index[0:2], ["A", "B"]].values, piece.values) @@ -1214,7 +1214,7 @@ def test_setitem_frame_mixed(self, float_string_frame): index=list(f.index[0:2]) + ["foo", "bar"], columns=["A", "B"], ) - key = (slice(None, 2), ["A", "B"]) + key = (f.index[slice(None, 2)], ["A", "B"]) f.loc[key] = piece tm.assert_almost_equal( f.loc[f.index[0:2:], ["A", "B"]].values, piece.values[0:2] @@ -1224,7 +1224,7 @@ def test_setitem_frame_mixed(self, float_string_frame): f = float_string_frame.copy() piece = f.loc[f.index[:2], ["A"]] piece.index = f.index[-2:] - key = (slice(-2, None), ["A", "B"]) + key = (f.index[slice(-2, None)], ["A", "B"]) f.loc[key] = piece piece["B"] = np.nan tm.assert_almost_equal(f.loc[f.index[-2:], ["A", "B"]].values, piece.values) @@ -1232,7 +1232,7 @@ def test_setitem_frame_mixed(self, float_string_frame): # ndarray f = float_string_frame.copy() piece = float_string_frame.loc[f.index[:2], ["A", "B"]] - key = (slice(-2, None), ["A", "B"]) + key = (f.index[slice(-2, None)], ["A", "B"]) f.loc[key] = piece.values tm.assert_almost_equal(f.loc[f.index[-2:], ["A", "B"]].values, piece.values) @@ -1857,7 +1857,7 @@ def test_setitem_datetimelike_with_inference(self): df = DataFrame(index=date_range("20130101", periods=4)) df["A"] = np.array([1 * one_hour] * 4, dtype="m8[ns]") df.loc[:, "B"] = np.array([2 * one_hour] * 4, dtype="m8[ns]") - df.loc[:3, "C"] = np.array([3 * one_hour] * 3, dtype="m8[ns]") + df.loc[df.index[:3], "C"] = np.array([3 * one_hour] * 3, dtype="m8[ns]") df.loc[:, "D"] = np.array([4 * one_hour] * 4, dtype="m8[ns]") df.loc[df.index[:3], "E"] = np.array([5 * one_hour] * 3, dtype="m8[ns]") df["F"] = np.timedelta64("NaT") diff --git a/pandas/tests/frame/methods/test_asof.py b/pandas/tests/frame/methods/test_asof.py index e2b417972638e..91d920c706bb6 100644 --- a/pandas/tests/frame/methods/test_asof.py +++ b/pandas/tests/frame/methods/test_asof.py @@ -21,7 +21,7 @@ class TestFrameAsof: def test_basic(self, date_range_frame): df = date_range_frame N = 50 - df.loc[15:30, "A"] = np.nan + df.loc[df.index[15:30], "A"] = np.nan dates = date_range("1/1/1990", periods=N * 3, freq="25s") result = df.asof(dates) @@ -41,7 +41,7 @@ def test_basic(self, date_range_frame): def test_subset(self, date_range_frame): N = 10 df = date_range_frame.iloc[:N].copy() - df.loc[4:8, "A"] = np.nan + df.loc[df.index[4:8], "A"] = np.nan dates = date_range("1/1/1990", periods=N * 3, freq="25s") # with a subset of A should be the same @@ -149,7 +149,7 @@ def test_is_copy(self, date_range_frame): # doesn't track the parent dataframe / doesn't give SettingWithCopy warnings df = date_range_frame N = 50 - df.loc[15:30, "A"] = np.nan + df.loc[df.index[15:30], "A"] = np.nan dates = date_range("1/1/1990", periods=N * 3, freq="25s") result = df.asof(dates) diff --git a/pandas/tests/frame/test_block_internals.py b/pandas/tests/frame/test_block_internals.py index d301ed969789e..799f001c24c62 100644 --- a/pandas/tests/frame/test_block_internals.py +++ b/pandas/tests/frame/test_block_internals.py @@ -481,7 +481,7 @@ def test_convert_objects(self, float_string_frame): length = len(float_string_frame) float_string_frame["J"] = "1." float_string_frame["K"] = "1" - float_string_frame.loc[0:5, ["J", "K"]] = "garbled" + float_string_frame.loc[float_string_frame.index[0:5], ["J", "K"]] = "garbled" converted = float_string_frame._convert(datetime=True, numeric=True) assert converted["H"].dtype == "float64" assert converted["I"].dtype == "int64" diff --git a/pandas/tests/indexing/test_indexing.py b/pandas/tests/indexing/test_indexing.py index 96fb1e8204f55..7aa4572cef412 100644 --- a/pandas/tests/indexing/test_indexing.py +++ b/pandas/tests/indexing/test_indexing.py @@ -1162,3 +1162,33 @@ def test_setitem_with_bool_mask_and_values_matching_n_trues_in_length(): result = ser expected = pd.Series([None] * 3 + list(range(5)) + [None] * 2).astype("object") tm.assert_series_equal(result, expected) + + +def test_loc_slice_disallows_positional(): + # GH#16121, GH#24612, GH#31810 + dti = pd.date_range("2016-01-01", periods=3) + df = pd.DataFrame(np.random.random((3, 2)), index=dti) + + ser = df[0] + + msg1 = ( + "cannot do slice indexing on DatetimeIndex with these " + r"indexers \[slice\(1, 3, None\)\] of type slice" + ) + msg2 = ( + "cannot do slice indexing on DatetimeIndex with these " + r"indexers \[1\] of type int" + ) + + for obj in [df, ser]: + with pytest.raises(TypeError, match=msg2): + obj.loc[1:3] + + with pytest.raises(TypeError, match=msg1): + obj.loc[1:3] = 1 + + with pytest.raises(TypeError, match=msg2): + df.loc[1:3, 1] + + with pytest.raises(TypeError, match=msg1): + df.loc[1:3, 1] = 2 diff --git a/pandas/tests/io/pytables/test_store.py b/pandas/tests/io/pytables/test_store.py index f56d042093886..dd9dda6952ae9 100644 --- a/pandas/tests/io/pytables/test_store.py +++ b/pandas/tests/io/pytables/test_store.py @@ -342,7 +342,7 @@ def test_repr(self, setup_path): df["timestamp2"] = Timestamp("20010103") df["datetime1"] = datetime.datetime(2001, 1, 2, 0, 0) df["datetime2"] = datetime.datetime(2001, 1, 3, 0, 0) - df.loc[3:6, ["obj1"]] = np.nan + df.loc[df.index[3:6], ["obj1"]] = np.nan df = df._consolidate()._convert(datetime=True) with catch_warnings(record=True): @@ -848,7 +848,7 @@ def test_put_mixed_type(self, setup_path): df["timestamp2"] = Timestamp("20010103") df["datetime1"] = datetime.datetime(2001, 1, 2, 0, 0) df["datetime2"] = datetime.datetime(2001, 1, 3, 0, 0) - df.loc[3:6, ["obj1"]] = np.nan + df.loc[df.index[3:6], ["obj1"]] = np.nan df = df._consolidate()._convert(datetime=True) with ensure_clean_store(setup_path) as store: @@ -1374,11 +1374,11 @@ def check_col(key, name, size): _maybe_remove(store, "df") df = tm.makeTimeDataFrame() df["string"] = "foo" - df.loc[1:4, "string"] = np.nan + df.loc[df.index[1:4], "string"] = np.nan df["string2"] = "bar" - df.loc[4:8, "string2"] = np.nan + df.loc[df.index[4:8], "string2"] = np.nan df["string3"] = "bah" - df.loc[1:, "string3"] = np.nan + df.loc[df.index[1:], "string3"] = np.nan store.append("df", df) result = store.select("df") tm.assert_frame_equal(result, df) @@ -1494,8 +1494,8 @@ def test_append_with_data_columns(self, setup_path): # data column selection with a string data_column df_new = df.copy() df_new["string"] = "foo" - df_new.loc[1:4, "string"] = np.nan - df_new.loc[5:6, "string"] = "bar" + df_new.loc[df_new.index[1:4], "string"] = np.nan + df_new.loc[df_new.index[5:6], "string"] = "bar" _maybe_remove(store, "df") store.append("df", df_new, data_columns=["string"]) result = store.select("df", "string='foo'") @@ -1576,12 +1576,12 @@ def check_col(key, name, size): # doc example df_dc = df.copy() df_dc["string"] = "foo" - df_dc.loc[4:6, "string"] = np.nan - df_dc.loc[7:9, "string"] = "bar" + df_dc.loc[df_dc.index[4:6], "string"] = np.nan + df_dc.loc[df_dc.index[7:9], "string"] = "bar" df_dc["string2"] = "cool" df_dc["datetime"] = Timestamp("20010102") df_dc = df_dc._convert(datetime=True) - df_dc.loc[3:5, ["A", "B", "datetime"]] = np.nan + df_dc.loc[df_dc.index[3:5], ["A", "B", "datetime"]] = np.nan _maybe_remove(store, "df_dc") store.append( @@ -1604,8 +1604,8 @@ def check_col(key, name, size): np.random.randn(8, 3), index=index, columns=["A", "B", "C"] ) df_dc["string"] = "foo" - df_dc.loc[4:6, "string"] = np.nan - df_dc.loc[7:9, "string"] = "bar" + df_dc.loc[df_dc.index[4:6], "string"] = np.nan + df_dc.loc[df_dc.index[7:9], "string"] = "bar" df_dc.loc[:, ["B", "C"]] = df_dc.loc[:, ["B", "C"]].abs() df_dc["string2"] = "cool" @@ -2026,7 +2026,7 @@ def test_table_mixed_dtypes(self, setup_path): df["timestamp2"] = Timestamp("20010103") df["datetime1"] = datetime.datetime(2001, 1, 2, 0, 0) df["datetime2"] = datetime.datetime(2001, 1, 3, 0, 0) - df.loc[3:6, ["obj1"]] = np.nan + df.loc[df.index[3:6], ["obj1"]] = np.nan df = df._consolidate()._convert(datetime=True) with ensure_clean_store(setup_path) as store: @@ -2202,7 +2202,7 @@ def test_invalid_terms(self, setup_path): df = tm.makeTimeDataFrame() df["string"] = "foo" - df.loc[0:4, "string"] = "bar" + df.loc[df.index[0:4], "string"] = "bar" store.put("df", df, format="table") @@ -3359,7 +3359,7 @@ def test_string_select(self, setup_path): # test string ==/!= df["x"] = "none" - df.loc[2:7, "x"] = "" + df.loc[df.index[2:7], "x"] = "" store.append("df", df, data_columns=["x"]) @@ -3381,7 +3381,7 @@ def test_string_select(self, setup_path): # int ==/!= df["int"] = 1 - df.loc[2:7, "int"] = 2 + df.loc[df.index[2:7], "int"] = 2 store.append("df3", df, data_columns=["int"]) @@ -3435,7 +3435,7 @@ def test_read_column(self, setup_path): # a data column with NaNs, result excludes the NaNs df3 = df.copy() df3["string"] = "foo" - df3.loc[4:6, "string"] = np.nan + df3.loc[df3.index[4:6], "string"] = np.nan store.append("df3", df3, data_columns=["string"]) result = store.select_column("df3", "string") tm.assert_almost_equal(result.values, df3["string"].values) From 7ee2eddc2906862fb4b668c5d7f5654c2f0ed247 Mon Sep 17 00:00:00 2001 From: jbrockmendel Date: Tue, 11 Feb 2020 14:39:07 -0800 Subject: [PATCH 05/11] update tests --- pandas/tests/indexing/test_loc.py | 1 + pandas/tests/series/indexing/test_datetime.py | 4 ++-- pandas/tests/series/methods/test_asof.py | 12 ++++++------ pandas/tests/series/test_arithmetic.py | 2 +- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/pandas/tests/indexing/test_loc.py b/pandas/tests/indexing/test_loc.py index 02652d993e0f3..15b04d7f57cbe 100644 --- a/pandas/tests/indexing/test_loc.py +++ b/pandas/tests/indexing/test_loc.py @@ -869,6 +869,7 @@ def test_loc_setitem_empty_append_raises(self): data = [1, 2] df = DataFrame(columns=["x", "y"]) + df.index = df.index.astype(np.int64) msg = ( r"None of \[Int64Index\(\[0, 1\], dtype='int64'\)\] " r"are in the \[index\]" diff --git a/pandas/tests/series/indexing/test_datetime.py b/pandas/tests/series/indexing/test_datetime.py index fc9d4ec5290a5..b5d04fd499c08 100644 --- a/pandas/tests/series/indexing/test_datetime.py +++ b/pandas/tests/series/indexing/test_datetime.py @@ -293,7 +293,7 @@ def test_getitem_setitem_datetimeindex(): result = ts.copy() result[ts.index[4:8]] = 0 - result[4:8] = ts[4:8] + result.iloc[4:8] = ts.iloc[4:8] tm.assert_series_equal(result, ts) # also test partial date slicing @@ -349,7 +349,7 @@ def test_getitem_setitem_periodindex(): result = ts.copy() result[ts.index[4:8]] = 0 - result[4:8] = ts[4:8] + result.iloc[4:8] = ts.iloc[4:8] tm.assert_series_equal(result, ts) diff --git a/pandas/tests/series/methods/test_asof.py b/pandas/tests/series/methods/test_asof.py index b121efd202744..40fb605bf0ae1 100644 --- a/pandas/tests/series/methods/test_asof.py +++ b/pandas/tests/series/methods/test_asof.py @@ -12,7 +12,7 @@ def test_basic(self): N = 50 rng = date_range("1/1/1990", periods=N, freq="53s") ts = Series(np.random.randn(N), index=rng) - ts[15:30] = np.nan + ts.iloc[15:30] = np.nan dates = date_range("1/1/1990", periods=N * 3, freq="25s") result = ts.asof(dates) @@ -37,8 +37,8 @@ def test_scalar(self): N = 30 rng = date_range("1/1/1990", periods=N, freq="53s") ts = Series(np.arange(N), index=rng) - ts[5:10] = np.NaN - ts[15:20] = np.NaN + ts.iloc[5:10] = np.NaN + ts.iloc[15:20] = np.NaN val1 = ts.asof(ts.index[7]) val2 = ts.asof(ts.index[19]) @@ -94,7 +94,7 @@ def test_periodindex(self): N = 50 rng = period_range("1/1/1990", periods=N, freq="H") ts = Series(np.random.randn(N), index=rng) - ts[15:30] = np.nan + ts.iloc[15:30] = np.nan dates = date_range("1/1/1990", periods=N * 3, freq="37min") result = ts.asof(dates) @@ -112,8 +112,8 @@ def test_periodindex(self): rs = result[mask] assert (rs == ts[lb]).all() - ts[5:10] = np.nan - ts[15:20] = np.nan + ts.iloc[5:10] = np.nan + ts.iloc[15:20] = np.nan val1 = ts.asof(ts.index[7]) val2 = ts.asof(ts.index[19]) diff --git a/pandas/tests/series/test_arithmetic.py b/pandas/tests/series/test_arithmetic.py index f3ffdc373e178..d2c69b9a5c8ec 100644 --- a/pandas/tests/series/test_arithmetic.py +++ b/pandas/tests/series/test_arithmetic.py @@ -73,7 +73,7 @@ def test_add_series_with_period_index(self): result = ts + ts[::2] expected = ts + ts - expected[1::2] = np.nan + expected.iloc[1::2] = np.nan tm.assert_series_equal(result, expected) result = ts + _permute(ts[::2]) From aa5a6a1b50ff900d08014dcd77d3f40039b54119 Mon Sep 17 00:00:00 2001 From: jbrockmendel Date: Sat, 22 Feb 2020 12:35:13 -0800 Subject: [PATCH 06/11] deprecate instead of raise --- pandas/core/indexes/base.py | 8 +++++- pandas/tests/indexing/test_indexing.py | 30 -------------------- pandas/tests/indexing/test_loc.py | 39 ++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 31 deletions(-) diff --git a/pandas/core/indexes/base.py b/pandas/core/indexes/base.py index 4397fe2465356..842d84b1d3a5c 100644 --- a/pandas/core/indexes/base.py +++ b/pandas/core/indexes/base.py @@ -3183,7 +3183,13 @@ def is_int(v): elif is_positional: if kind == "loc": # GH#16121, GH#24612, GH#31810 - self._invalid_indexer("slice", key) + warnings.warn( + "Slicing .loc with a positional slice is not supported, " + "and will raise TypeError in a future version. " + "Use .iloc instead.", + FutureWarning, + stacklevel=5, + ) indexer = key else: indexer = self.slice_indexer(start, stop, step, kind=kind) diff --git a/pandas/tests/indexing/test_indexing.py b/pandas/tests/indexing/test_indexing.py index 501addf186ba3..8af0fe548e48a 100644 --- a/pandas/tests/indexing/test_indexing.py +++ b/pandas/tests/indexing/test_indexing.py @@ -1136,33 +1136,3 @@ def test_setitem_with_bool_mask_and_values_matching_n_trues_in_length(): result = ser expected = pd.Series([None] * 3 + list(range(5)) + [None] * 2).astype("object") tm.assert_series_equal(result, expected) - - -def test_loc_slice_disallows_positional(): - # GH#16121, GH#24612, GH#31810 - dti = pd.date_range("2016-01-01", periods=3) - df = pd.DataFrame(np.random.random((3, 2)), index=dti) - - ser = df[0] - - msg1 = ( - "cannot do slice indexing on DatetimeIndex with these " - r"indexers \[slice\(1, 3, None\)\] of type slice" - ) - msg2 = ( - "cannot do slice indexing on DatetimeIndex with these " - r"indexers \[1\] of type int" - ) - - for obj in [df, ser]: - with pytest.raises(TypeError, match=msg2): - obj.loc[1:3] - - with pytest.raises(TypeError, match=msg1): - obj.loc[1:3] = 1 - - with pytest.raises(TypeError, match=msg2): - df.loc[1:3, 1] - - with pytest.raises(TypeError, match=msg1): - df.loc[1:3, 1] = 2 diff --git a/pandas/tests/indexing/test_loc.py b/pandas/tests/indexing/test_loc.py index 92211c5cea029..1d10a4ab517a5 100644 --- a/pandas/tests/indexing/test_loc.py +++ b/pandas/tests/indexing/test_loc.py @@ -976,3 +976,42 @@ def test_loc_mixed_int_float(): result = ser.loc[1] assert result == 0 + + +def test_loc_with_positional_slice_deprecation(): + # GH#31840 + ser = pd.Series(range(4), index=["A", "B", "C", "D"]) + + with tm.assert_produces_warning(FutureWarning): + ser.loc[:3] = 2 + + expected = pd.Series([2, 2, 2, 3], index=["A", "B", "C", "D"]) + tm.assert_series_equal(ser, expected) + + +def test_loc_slice_disallows_positional(): + # GH#16121, GH#24612, GH#31810 + dti = pd.date_range("2016-01-01", periods=3) + df = pd.DataFrame(np.random.random((3, 2)), index=dti) + + ser = df[0] + + msg = ( + "cannot do slice indexing on DatetimeIndex with these " + r"indexers \[1\] of type int" + ) + + for obj in [df, ser]: + with pytest.raises(TypeError, match=msg): + obj.loc[1:3] + + with tm.assert_produces_warning(FutureWarning): + # GH#31840 deprecated incorrect behavior + obj.loc[1:3] = 1 + + with pytest.raises(TypeError, match=msg): + df.loc[1:3, 1] + + with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): + # GH#31840 deprecated incorrect behavior + df.loc[1:3, 1] = 2 From 87f424dd29cd73c2c407895203c06dab6d7083b7 Mon Sep 17 00:00:00 2001 From: jbrockmendel Date: Sat, 22 Feb 2020 12:36:48 -0800 Subject: [PATCH 07/11] whatsnew --- doc/source/whatsnew/v1.1.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v1.1.0.rst b/doc/source/whatsnew/v1.1.0.rst index 7449c62a5ad31..ba37a08376591 100644 --- a/doc/source/whatsnew/v1.1.0.rst +++ b/doc/source/whatsnew/v1.1.0.rst @@ -77,7 +77,7 @@ Deprecations ~~~~~~~~~~~~ - Lookups on a :class:`Series` with a single-item list containing a slice (e.g. ``ser[[slice(0, 4)]]``) are deprecated, will raise in a future version. Either convert the list to tuple, or pass the slice directly instead (:issue:`31333`) - :meth:`DataFrame.mean` and :meth:`DataFrame.median` with ``numeric_only=None`` will include datetime64 and datetime64tz columns in a future version (:issue:`29941`) -- +- Setting values with ``.loc`` using a positional slice is deprecated and will raise in a future version. Use ``.iloc`` instead (:issue:`31840`) - .. --------------------------------------------------------------------------- From 75f2db6a23ac4f7b1315387f3047dc2762fcd889 Mon Sep 17 00:00:00 2001 From: jbrockmendel Date: Mon, 24 Feb 2020 09:23:16 -0800 Subject: [PATCH 08/11] update test --- pandas/tests/frame/test_apply.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/tests/frame/test_apply.py b/pandas/tests/frame/test_apply.py index fe6abef97acc4..11705cd77a325 100644 --- a/pandas/tests/frame/test_apply.py +++ b/pandas/tests/frame/test_apply.py @@ -339,7 +339,7 @@ def test_apply_yield_list(self, float_frame): tm.assert_frame_equal(result, float_frame) def test_apply_reduce_Series(self, float_frame): - float_frame.loc[::2, "A"] = np.nan + float_frame["A"].iloc[::2] = np.nan expected = float_frame.mean(1) result = float_frame.apply(np.mean, axis=1) tm.assert_series_equal(result, expected) From 2933f2c804fed6b7d6343eb2c93e949755f63a6c Mon Sep 17 00:00:00 2001 From: jbrockmendel Date: Mon, 2 Mar 2020 08:45:17 -0800 Subject: [PATCH 09/11] Update pandas/core/indexes/base.py Co-Authored-By: Tom Augspurger --- pandas/core/indexes/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/core/indexes/base.py b/pandas/core/indexes/base.py index 94cfc6934cbba..af5017d5a462e 100644 --- a/pandas/core/indexes/base.py +++ b/pandas/core/indexes/base.py @@ -3183,7 +3183,7 @@ def is_int(v): if kind == "loc": # GH#16121, GH#24612, GH#31810 warnings.warn( - "Slicing .loc with a positional slice is not supported, " + "Slicing a positional slice with .loc is not supported, " "and will raise TypeError in a future version. " "Use .iloc instead.", FutureWarning, From 46402f81a6ab32fa9444090eb1a2085925775f59 Mon Sep 17 00:00:00 2001 From: jbrockmendel Date: Mon, 2 Mar 2020 08:47:42 -0800 Subject: [PATCH 10/11] update message --- doc/source/whatsnew/v1.1.0.rst | 2 +- pandas/core/indexes/base.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/source/whatsnew/v1.1.0.rst b/doc/source/whatsnew/v1.1.0.rst index d8913198ecb4e..68ce1cab77aa5 100644 --- a/doc/source/whatsnew/v1.1.0.rst +++ b/doc/source/whatsnew/v1.1.0.rst @@ -169,7 +169,7 @@ Deprecations ~~~~~~~~~~~~ - Lookups on a :class:`Series` with a single-item list containing a slice (e.g. ``ser[[slice(0, 4)]]``) are deprecated, will raise in a future version. Either convert the list to tuple, or pass the slice directly instead (:issue:`31333`) - :meth:`DataFrame.mean` and :meth:`DataFrame.median` with ``numeric_only=None`` will include datetime64 and datetime64tz columns in a future version (:issue:`29941`) -- Setting values with ``.loc`` using a positional slice is deprecated and will raise in a future version. Use ``.iloc`` instead (:issue:`31840`) +- Setting values with ``.loc`` using a positional slice is deprecated and will raise in a future version. Use ``.loc`` with labels or ``.iloc`` with positions instead (:issue:`31840`) - .. --------------------------------------------------------------------------- diff --git a/pandas/core/indexes/base.py b/pandas/core/indexes/base.py index f0918589ec7fb..9ab192c6720ac 100644 --- a/pandas/core/indexes/base.py +++ b/pandas/core/indexes/base.py @@ -3189,7 +3189,7 @@ def is_int(v): warnings.warn( "Slicing a positional slice with .loc is not supported, " "and will raise TypeError in a future version. " - "Use .iloc instead.", + "Use .loc with labels or .iloc with positions instead.", FutureWarning, stacklevel=5, ) From a14aa8d39c92aed1ec832fe7ccaefe49c1472f47 Mon Sep 17 00:00:00 2001 From: jbrockmendel Date: Wed, 4 Mar 2020 16:55:11 -0800 Subject: [PATCH 11/11] optimize stacklevel for DataFrame --- pandas/core/indexes/base.py | 2 +- pandas/tests/indexing/test_loc.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pandas/core/indexes/base.py b/pandas/core/indexes/base.py index 60ef717153804..e32f597c9e378 100644 --- a/pandas/core/indexes/base.py +++ b/pandas/core/indexes/base.py @@ -3147,7 +3147,7 @@ def is_int(v): "and will raise TypeError in a future version. " "Use .loc with labels or .iloc with positions instead.", FutureWarning, - stacklevel=5, + stacklevel=6, ) indexer = key else: diff --git a/pandas/tests/indexing/test_loc.py b/pandas/tests/indexing/test_loc.py index ee90189555550..3073fe085de15 100644 --- a/pandas/tests/indexing/test_loc.py +++ b/pandas/tests/indexing/test_loc.py @@ -982,7 +982,7 @@ def test_loc_with_positional_slice_deprecation(): # GH#31840 ser = pd.Series(range(4), index=["A", "B", "C", "D"]) - with tm.assert_produces_warning(FutureWarning): + with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): ser.loc[:3] = 2 expected = pd.Series([2, 2, 2, 3], index=["A", "B", "C", "D"]) @@ -1005,13 +1005,13 @@ def test_loc_slice_disallows_positional(): with pytest.raises(TypeError, match=msg): obj.loc[1:3] - with tm.assert_produces_warning(FutureWarning): + with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): # GH#31840 deprecated incorrect behavior obj.loc[1:3] = 1 with pytest.raises(TypeError, match=msg): df.loc[1:3, 1] - with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): + with tm.assert_produces_warning(FutureWarning): # GH#31840 deprecated incorrect behavior df.loc[1:3, 1] = 2