Skip to content

Commit 5f7c9e9

Browse files
committed
DEPR: removal of deprecation warnings for float indexers in a positional setting,
and raise a TypeError, xref pandas-dev#4892 BUG: index type coercion when setting with an integer-like closes pandas-dev#11836
1 parent 0181ef4 commit 5f7c9e9

File tree

10 files changed

+547
-227
lines changed

10 files changed

+547
-227
lines changed

doc/source/release.rst

+1
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ Highlights include:
5252
- ``pd.test()`` top-level nose test runner is available (:issue:`4327`)
5353
- Adding support for a ``RangeIndex`` as a specialized form of the ``Int64Index`` for memory savings, see :ref:`here <whatsnew_0180.enhancements.rangeindex>`.
5454
- API breaking ``.resample`` changes to make it more ``.groupby`` like, see :ref:`here <whatsnew_0180.breaking.resample>`.
55+
- Removal of support for deprecated float indexers; these will now raise a ``TypeError``, see :ref:`here <whatsnew_0180.enhancements.float_indexers>`.
5556

5657
See the :ref:`v0.18.0 Whatsnew <whatsnew_0180>` overview for an extensive list
5758
of all enhancements and bugs that have been fixed in 0.17.1.

doc/source/whatsnew/v0.18.0.txt

+38-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ Highlights include:
2121
- ``pd.test()`` top-level nose test runner is available (:issue:`4327`)
2222
- Adding support for a ``RangeIndex`` as a specialized form of the ``Int64Index`` for memory savings, see :ref:`here <whatsnew_0180.enhancements.rangeindex>`.
2323
- API breaking ``.resample`` changes to make it more ``.groupby`` like, see :ref:`here <whatsnew_0180.breaking.resample>`.
24+
- Removal of support for deprecated float indexers; these will now raise a ``TypeError``, see :ref:`here <whatsnew_0180.enhancements.float_indexers>`.
2425

2526
Check the :ref:`API Changes <whatsnew_0180.api_breaking>` and :ref:`deprecations <whatsnew_0180.deprecations>` before updating.
2627

@@ -865,9 +866,45 @@ Deprecations
865866
is better handled by matplotlib's `style sheets`_ (:issue:`11783`).
866867

867868

869+
.. _style sheets: http://matplotlib.org/users/style_sheets.html
868870

871+
.. _whatsnew_0180.float_indexers:
869872

870-
.. _style sheets: http://matplotlib.org/users/style_sheets.html
873+
Removal of deprecated float indexers
874+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
875+
876+
In :issue:`4892` indexing with floating point numbers on a non-``Float64Index`` was deprecated (in version 0.14.0).
877+
In 0.18.0, this deprecation warning is removed and these will now raise a ``TypeError``. (:issue:`12165`)
878+
879+
Previous Behavior:
880+
881+
.. code-block:
882+
883+
In [1]: s = Series([1,2,3])
884+
In [2]: s[1.0]
885+
FutureWarning: scalar indexers for index type Int64Index should be integers and not floating point
886+
Out[2]: 2
887+
888+
In [3]: s.iloc[1.0]
889+
FutureWarning: scalar indexers for index type Int64Index should be integers and not floating point
890+
Out[3]: 2
891+
892+
In [4]: s.loc[1.0]
893+
FutureWarning: scalar indexers for index type Int64Index should be integers and not floating point
894+
Out[4]: 2
895+
896+
New Behavior:
897+
898+
.. code-block:
899+
900+
In [4]: s[1.0]
901+
TypeError: cannot do label indexing on <class 'pandas.indexes.range.RangeIndex'> with these indexers [1.0] of <type 'float'>
902+
903+
In [4]: s.iloc[1.0]
904+
TypeError: cannot do label indexing on <class 'pandas.indexes.range.RangeIndex'> with these indexers [1.0] of <type 'float'>
905+
906+
In [4]: s.loc[1.0]
907+
TypeError: cannot do label indexing on <class 'pandas.indexes.range.RangeIndex'> with these indexers [1.0] of <type 'float'>
871908

872909
.. _whatsnew_0180.prior_deprecations:
873910

pandas/core/indexing.py

+30-8
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,11 @@ def _get_setitem_indexer(self, key):
115115

116116
try:
117117
return self._convert_to_indexer(key, is_setter=True)
118-
except TypeError:
118+
except TypeError as e:
119+
120+
# invalid indexer type vs 'other' indexing errors
121+
if 'cannot do' in str(e):
122+
raise
119123
raise IndexingError(key)
120124

121125
def __setitem__(self, key, value):
@@ -312,6 +316,18 @@ def _setitem_with_indexer(self, indexer, value):
312316
index = self.obj.index
313317
new_index = index.insert(len(index), indexer)
314318

319+
# we have a coerced indexer, e.g. a float
320+
# that matches in an Int64Index, so
321+
# we will not create a duplicate index, rather
322+
# index to that element
323+
# e.g. 0.0 -> 0
324+
# GH12246
325+
if index.is_unique:
326+
new_indexer = index.get_indexer([new_index[-1]])
327+
if (new_indexer != -1).any():
328+
return self._setitem_with_indexer(new_indexer,
329+
value)
330+
315331
# this preserves dtype of the value
316332
new_values = Series([value])._values
317333
if len(self.obj._values):
@@ -1091,8 +1107,17 @@ def _convert_to_indexer(self, obj, axis=0, is_setter=False):
10911107
"""
10921108
labels = self.obj._get_axis(axis)
10931109

1094-
# if we are a scalar indexer and not type correct raise
1095-
obj = self._convert_scalar_indexer(obj, axis)
1110+
if isinstance(obj, slice):
1111+
return self._convert_slice_indexer(obj, axis)
1112+
1113+
# try to find out correct indexer, if not type correct raise
1114+
try:
1115+
obj = self._convert_scalar_indexer(obj, axis)
1116+
except TypeError:
1117+
1118+
# but we will allow setting
1119+
if is_setter:
1120+
pass
10961121

10971122
# see if we are positional in nature
10981123
is_int_index = labels.is_integer()
@@ -1131,10 +1156,7 @@ def _convert_to_indexer(self, obj, axis=0, is_setter=False):
11311156

11321157
return obj
11331158

1134-
if isinstance(obj, slice):
1135-
return self._convert_slice_indexer(obj, axis)
1136-
1137-
elif is_nested_tuple(obj, labels):
1159+
if is_nested_tuple(obj, labels):
11381160
return labels.get_locs(obj)
11391161
elif is_list_like_indexer(obj):
11401162
if is_bool_indexer(obj):
@@ -1278,7 +1300,7 @@ def _get_slice_axis(self, slice_obj, axis=0):
12781300

12791301
labels = obj._get_axis(axis)
12801302
indexer = labels.slice_indexer(slice_obj.start, slice_obj.stop,
1281-
slice_obj.step)
1303+
slice_obj.step, kind=self.name)
12821304

12831305
if isinstance(indexer, slice):
12841306
return self._slice(indexer, axis=axis, kind='iloc')

pandas/indexes/base.py

+67-39
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,12 @@
2121
from pandas.core.missing import _clean_reindex_fill_method
2222
from pandas.core.common import (isnull, array_equivalent,
2323
is_object_dtype, is_datetimetz, ABCSeries,
24-
ABCPeriodIndex,
24+
ABCPeriodIndex, ABCMultiIndex,
2525
_values_from_object, is_float, is_integer,
2626
is_iterator, is_categorical_dtype,
2727
_ensure_object, _ensure_int64, is_bool_indexer,
2828
is_list_like, is_bool_dtype,
29-
is_integer_dtype)
29+
is_integer_dtype, is_float_dtype)
3030
from pandas.core.strings import StringAccessorMixin
3131

3232
from pandas.core.config import get_option
@@ -162,7 +162,46 @@ def __new__(cls, data=None, dtype=None, copy=False, name=None,
162162

163163
if dtype is not None:
164164
try:
165-
data = np.array(data, dtype=dtype, copy=copy)
165+
166+
# we need to avoid having numpy coerce
167+
# things that look like ints/floats to ints unless
168+
# they are actually ints, e.g. '0' and 0.0
169+
# should not be coerced
170+
# GH 11836
171+
if is_integer_dtype(dtype):
172+
inferred = lib.infer_dtype(data)
173+
if inferred == 'integer':
174+
data = np.array(data, copy=copy, dtype=dtype)
175+
elif inferred in ['floating', 'mixed-integer-float']:
176+
177+
# if we are actually all equal to integers
178+
# then coerce to integer
179+
from .numeric import Int64Index, Float64Index
180+
try:
181+
res = data.astype('i8')
182+
if (res == data).all():
183+
return Int64Index(res, copy=copy,
184+
name=name)
185+
except (TypeError, ValueError):
186+
pass
187+
188+
# return an actual float index
189+
return Float64Index(data, copy=copy, dtype=dtype,
190+
name=name)
191+
192+
elif inferred == 'string':
193+
pass
194+
else:
195+
data = data.astype(dtype)
196+
elif is_float_dtype(dtype):
197+
inferred = lib.infer_dtype(data)
198+
if inferred == 'string':
199+
pass
200+
else:
201+
data = data.astype(dtype)
202+
else:
203+
data = np.array(data, dtype=dtype, copy=copy)
204+
166205
except (TypeError, ValueError):
167206
pass
168207

@@ -930,35 +969,32 @@ def _convert_scalar_indexer(self, key, kind=None):
930969
kind : optional, type of the indexing operation (loc/ix/iloc/None)
931970
932971
right now we are converting
933-
floats -> ints if the index supports it
934972
"""
935973

936-
def to_int():
937-
ikey = int(key)
938-
if ikey != key:
939-
return self._invalid_indexer('label', key)
940-
return ikey
941-
942974
if kind == 'iloc':
943975
if is_integer(key):
944976
return key
945-
elif is_float(key):
946-
key = to_int()
947-
warnings.warn("scalar indexers for index type {0} should be "
948-
"integers and not floating point".format(
949-
type(self).__name__),
950-
FutureWarning, stacklevel=5)
951-
return key
952977
return self._invalid_indexer('label', key)
978+
else:
953979

954-
if is_float(key):
955-
if isnull(key):
956-
return self._invalid_indexer('label', key)
957-
warnings.warn("scalar indexers for index type {0} should be "
958-
"integers and not floating point".format(
959-
type(self).__name__),
960-
FutureWarning, stacklevel=3)
961-
return to_int()
980+
if len(self):
981+
982+
# we can safely disallow
983+
# if we are not a MultiIndex
984+
# or a Float64Index
985+
# or have mixed inferred type (IOW we have the possiblity
986+
# of a float in with say strings)
987+
if is_float(key):
988+
if not (isinstance(self, ABCMultiIndex,) or
989+
self.is_floating() or self.is_mixed()):
990+
return self._invalid_indexer('label', key)
991+
992+
# we can disallow integers with loc
993+
# if could not contain and integer
994+
elif is_integer(key) and kind == 'loc':
995+
if not (isinstance(self, ABCMultiIndex,) or
996+
self.holds_integer() or self.is_mixed()):
997+
return self._invalid_indexer('label', key)
962998

963999
return key
9641000

@@ -991,14 +1027,6 @@ def f(c):
9911027
v = getattr(key, c)
9921028
if v is None or is_integer(v):
9931029
return v
994-
995-
# warn if it's a convertible float
996-
if v == int(v):
997-
warnings.warn("slice indexers when using iloc should be "
998-
"integers and not floating point",
999-
FutureWarning, stacklevel=7)
1000-
return int(v)
1001-
10021030
self._invalid_indexer('slice {0} value'.format(c), v)
10031031

10041032
return slice(*[f(c) for c in ['start', 'stop', 'step']])
@@ -1057,7 +1085,7 @@ def is_int(v):
10571085
indexer = key
10581086
else:
10591087
try:
1060-
indexer = self.slice_indexer(start, stop, step)
1088+
indexer = self.slice_indexer(start, stop, step, kind=kind)
10611089
except Exception:
10621090
if is_index_slice:
10631091
if self.is_integer():
@@ -1891,10 +1919,7 @@ def get_value(self, series, key):
18911919
s = _values_from_object(series)
18921920
k = _values_from_object(key)
18931921

1894-
# prevent integer truncation bug in indexing
1895-
if is_float(k) and not self.is_floating():
1896-
raise KeyError
1897-
1922+
k = self._convert_scalar_indexer(k, kind='getitem')
18981923
try:
18991924
return self._engine.get_value(s, k,
19001925
tz=getattr(series.dtype, 'tz', None))
@@ -2236,6 +2261,7 @@ def reindex(self, target, method=None, level=None, limit=None,
22362261
if self.equals(target):
22372262
indexer = None
22382263
else:
2264+
22392265
if self.is_unique:
22402266
indexer = self.get_indexer(target, method=method,
22412267
limit=limit,
@@ -2722,7 +2748,9 @@ def _maybe_cast_slice_bound(self, label, side, kind):
27222748
# datetimelike Indexes
27232749
# reject them
27242750
if is_float(label):
2725-
self._invalid_indexer('slice', label)
2751+
if not (kind in ['ix'] and (self.holds_integer() or
2752+
self.is_floating())):
2753+
self._invalid_indexer('slice', label)
27262754

27272755
# we are trying to find integer bounds on a non-integer based index
27282756
# this is rejected (generally .loc gets you here)

pandas/indexes/numeric.py

+20-3
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,14 @@ def _maybe_cast_slice_bound(self, label, side, kind):
4242
"""
4343

4444
# we are a numeric index, so we accept
45-
# integer/floats directly
46-
if not (com.is_integer(label) or com.is_float(label)):
47-
self._invalid_indexer('slice', label)
45+
# integer directly
46+
if com.is_integer(label):
47+
pass
48+
49+
# disallow floats only if we not-strict
50+
elif com.is_float(label):
51+
if not (self.is_floating() or kind in ['ix']):
52+
self._invalid_indexer('slice', label)
4853

4954
return label
5055

@@ -200,6 +205,18 @@ def __new__(cls, data=None, dtype=None, copy=False, name=None,
200205

201206
if dtype is None:
202207
dtype = np.float64
208+
dtype = np.dtype(dtype)
209+
210+
# allow integer / object dtypes to be passed, but coerce to float64
211+
if dtype.kind in ['i', 'O']:
212+
dtype = np.float64
213+
214+
elif dtype.kind in ['f']:
215+
pass
216+
217+
else:
218+
raise TypeError("cannot support {0} dtype in "
219+
"Float64Index".format(dtype))
203220

204221
try:
205222
subarr = np.array(data, dtype=dtype, copy=copy)

0 commit comments

Comments
 (0)