Skip to content

Commit

Permalink
API: warning to raise KeyError in the future if not all elements of a…
Browse files Browse the repository at this point in the history
… list are selected via .loc

closes #15747
  • Loading branch information
jreback committed Oct 1, 2017
1 parent 408ecd2 commit 9020827
Show file tree
Hide file tree
Showing 16 changed files with 372 additions and 64 deletions.
109 changes: 108 additions & 1 deletion doc/source/indexing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -333,8 +333,15 @@ Selection By Label
dfl.loc['20130102':'20130104']
.. warning::

Starting in 0.21.0, pandas will show a ``FutureWarning`` if indexing with a list-of-lables and not ALL labels are present. In the future
this will raise a ``KeyError``. See :ref:`list-like Using loc with missing keys in a list is Deprecated <indexing.deprecate_loc_reindex_listlike>`

pandas provides a suite of methods in order to have **purely label based indexing**. This is a strict inclusion based protocol.
**At least 1** of the labels for which you ask, must be in the index or a ``KeyError`` will be raised! When slicing, both the start bound **AND** the stop bound are *included*, if present in the index. Integers are valid labels, but they refer to the label **and not the position**.
All of the labels for which you ask, must be in the index or a ``KeyError`` will be raised!
When slicing, both the start bound **AND** the stop bound are *included*, if present in the index.
Integers are valid labels, but they refer to the label **and not the position**.

The ``.loc`` attribute is the primary access method. The following are valid inputs:

Expand Down Expand Up @@ -635,6 +642,106 @@ For getting *multiple* indexers, using ``.get_indexer``
dfd.iloc[[0, 2], dfd.columns.get_indexer(['A', 'B'])]
.. _indexing.deprecate_loc_reindex_listlike:

Indexing with missing list-of-labels is Deprecated
--------------------------------------------------

.. warning::

Starting in 0.21.0, using ``.loc`` or ``[]`` with a list-like containing one or more missing labels, is deprecated, in favor of ``.reindex``.

In prior versions, using ``.loc[list-of-labels]`` would work as long as *at least 1* of the keys was found (otherwise it
would raise a ``KeyError``). This behavior is deprecated and will show a warning message pointing to this section. The
recommeded alternative is to use ``.reindex()``.

For example.

.. ipython:: python
s = pd.Series([1, 2, 3])
s
Selection with all keys found is unchanged.

.. ipython:: python
s.loc[[1, 2]]
Previous Behavior

.. code-block:: ipython
In [4]: s.loc[[1, 2, 3]]
Out[4]:
1 2.0
2 3.0
3 NaN
dtype: float64
Current Behavior

In [4]: s.loc[[1, 2, 3]]
Passing list-likes to .loc with any non-matching elements will raise
KeyError in the future, you can use .reindex() as an alternative.

See the documentation here:
http://pandas.pydata.org/pandas-docs/stable/indexing.html#deprecate-loc-reindex-listlike

Out[4]:
1 2.0
2 3.0
3 NaN
dtype: float64


Reindexing
~~~~~~~~~~

The idiomatic way to achieve selecting potentially not-found elmenents is via ``.reindex()``. See also the section on :ref:`reindexing <basics.reindexing>`.

.. ipython:: python
s.reindex([1, 2, 3])
Alternatively, if you want to select only *valid* keys, the following is idiomatic and efficient; it is guaranteed to preserve the dtype of the selection.

.. ipython:: python
labels = [1, 2, 3]
s.loc[s.index.intersection(labels)]
Having a duplicated index will raise for a ``.reindex()``:

.. ipython:: python
s = pd.Series(np.arange(4), index=['a', 'a', 'b', 'c'])
labels = ['c', 'd']
.. code-block:: python
In [17]: s.reindex(labels)
ValueError: cannot reindex from a duplicate axis
Generally, you can interesect the desired labels with the current
axis, and then reindex.

.. ipython:: python
s.loc[s.index.intersection(labels)].reindex(labels)
However, this would *still* raise if your resulting index is duplicated.

.. code-block:: python
In [41]: labels = ['a', 'd']
In [42]: s.loc[s.index.intersection(labels)].reindex(labels)
ValueError: cannot reindex from a duplicate axis
.. _indexing.basics.partial_setting:

Selecting Random Samples
Expand Down
24 changes: 19 additions & 5 deletions doc/source/whatsnew/v0.15.0.txt
Original file line number Diff line number Diff line change
Expand Up @@ -676,10 +676,19 @@ Other notable API changes:

Both will now return a frame reindex by [1,3]. E.g.

.. ipython:: python
.. code-block:: ipython

df.loc[[1,3]]
df.loc[[1,3],:]
In [3]: df.loc[[1,3]]
Out[3]:
0
1 a
3 NaN

In [4]: df.loc[[1,3],:]
Out[4]:
0
1 a
3 NaN

This can also be seen in multi-axis indexing with a ``Panel``.

Expand All @@ -693,9 +702,14 @@ Other notable API changes:

The following would raise ``KeyError`` prior to 0.15.0:

.. ipython:: python
.. code-block:: ipython

p.loc[['ItemA','ItemD'],:,'D']
In [5]:
Out[5]:
ItemA ItemD
1 3 NaN
2 7 NaN
3 11 NaN

Furthermore, ``.loc`` will raise If no values are found in a multi-index with a list-like indexer:

Expand Down
57 changes: 57 additions & 0 deletions doc/source/whatsnew/v0.21.0.txt
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,63 @@ We have updated our minimum supported versions of dependencies (:issue:`15206`,
| Bottleneck | 1.0.0 | |
+--------------+-----------------+----------+

.. _whatsnew_0210.api_breaking.loc:

Indexing with missing list-of-labels is Deprecated
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Previously, selecting at least 1 valid label with a list-like indexer would always succeed, returning ``NaN`` for missing labels.
This will now show a ``FutureWarning``, in the future this will raise a ``KeyError`` (:issue:`15747`).
This warning will trigger on a ``DataFrame`` or a ``Series`` for using ``.loc[]`` or ``[[]]`` when passing a list-of-labels with at least 1 missing label.
See the :ref:`deprecation docs <indexing.deprecate_loc_reindex_listlike>`.


.. ipython:: python

s = pd.Series([1, 2, 3])
s

Previous Behavior

.. code-block:: ipython


In [4]: s.loc[[1, 2, 3]]
Out[4]:
1 2.0
2 3.0
3 NaN
dtype: float64


Current Behavior

In [4]: s.loc[[1, 2, 3]]
Passing list-likes to .loc or [] with any missing label will raise
KeyError in the future, you can use .reindex() as an alternative.

See the documentation here:
http://pandas.pydata.org/pandas-docs/stable/indexing.html#deprecate-loc-reindex-listlike

Out[4]:
1 2.0
2 3.0
3 NaN
dtype: float64

The idiomatic way to achieve selecting potentially not-found elmenents is via ``.reindex()``

.. ipython:: python

s.reindex([1, 2, 3])

Selection with all keys found is unchanged.

.. ipython:: python

s.loc[[1, 2]]


.. _whatsnew_0210.api_breaking.pandas_eval:

Improved error handling during item assignment in pd.eval
Expand Down
32 changes: 26 additions & 6 deletions pandas/core/indexing.py
Original file line number Diff line number Diff line change
Expand Up @@ -1419,13 +1419,33 @@ def _has_valid_type(self, key, axis):
if isinstance(key, tuple) and isinstance(ax, MultiIndex):
return True

# TODO: don't check the entire key unless necessary
if (not is_iterator(key) and len(key) and
np.all(ax.get_indexer_for(key) < 0)):
if not is_iterator(key) and len(key):

raise KeyError(u"None of [{key}] are in the [{axis}]"
.format(key=key,
axis=self.obj._get_axis_name(axis)))
# True indicates missing values
missing = ax.get_indexer_for(key) < 0

if np.any(missing):
if len(key) == 1 or np.all(missing):
raise KeyError(
u"None of [{key}] are in the [{axis}]".format(
key=key, axis=self.obj._get_axis_name(axis)))
else:

# we skip the warning on Categorical/Interval
# as this check is actually done (check for
# non-missing values), but a bit later in the
# code, so we want to avoid warning & then
# just raising
_missing_key_warning = textwrap.dedent("""
Passing list-likes to .loc or [] with any missing label will raise
KeyError in the future, you can use .reindex() as an alternative.
See the documentation here:
http://pandas.pydata.org/pandas-docs/stable/indexing.html#deprecate-loc-reindex-listlike""") # noqa

if not (ax.is_categorical() or ax.is_interval()):
warnings.warn(_missing_key_warning,
FutureWarning, stacklevel=5)

return True

Expand Down
2 changes: 1 addition & 1 deletion pandas/core/series.py
Original file line number Diff line number Diff line change
Expand Up @@ -691,7 +691,7 @@ def _get_with(self, key):

if key_type == 'integer':
if self.index.is_integer() or self.index.is_floating():
return self.reindex(key)
return self.loc[key]
else:
return self._get_values(key)
elif key_type == 'boolean':
Expand Down
10 changes: 9 additions & 1 deletion pandas/io/formats/excel.py
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,15 @@ def __init__(self, df, na_rep='', float_format=None, cols=None,
self.styler = None
self.df = df
if cols is not None:
self.df = df.loc[:, cols]

# all missing, raise
if not len(Index(cols) & df.columns):
raise KeyError

# 1 missing is ok
# TODO(jreback) this should raise
# on *any* missing columns
self.df = df.reindex(columns=cols)
self.columns = self.df.columns
self.float_format = float_format
self.index = index
Expand Down
3 changes: 2 additions & 1 deletion pandas/tests/indexing/test_categorical.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,8 @@ def test_loc_listlike(self):
assert_frame_equal(result, expected, check_index_type=True)

# not all labels in the categories
pytest.raises(KeyError, lambda: self.df2.loc[['a', 'd']])
with pytest.raises(KeyError):
self.df2.loc[['a', 'd']]

def test_loc_listlike_dtypes(self):
# GH 11586
Expand Down
8 changes: 6 additions & 2 deletions pandas/tests/indexing/test_datetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,9 @@ def test_series_partial_set_datetime(self):
Timestamp('2011-01-03')]
exp = Series([np.nan, 0.2, np.nan],
index=pd.DatetimeIndex(keys, name='idx'), name='s')
tm.assert_series_equal(ser.loc[keys], exp, check_index_type=True)
with tm.assert_produces_warning(FutureWarning,
check_stacklevel=False):
tm.assert_series_equal(ser.loc[keys], exp, check_index_type=True)

def test_series_partial_set_period(self):
# GH 11497
Expand All @@ -248,5 +250,7 @@ def test_series_partial_set_period(self):
pd.Period('2011-01-03', freq='D')]
exp = Series([np.nan, 0.2, np.nan],
index=pd.PeriodIndex(keys, name='idx'), name='s')
result = ser.loc[keys]
with tm.assert_produces_warning(FutureWarning,
check_stacklevel=False):
result = ser.loc[keys]
tm.assert_series_equal(result, exp)
3 changes: 2 additions & 1 deletion pandas/tests/indexing/test_iloc.py
Original file line number Diff line number Diff line change
Expand Up @@ -617,7 +617,8 @@ def test_iloc_non_unique_indexing(self):
expected = DataFrame(new_list)
expected = pd.concat([expected, DataFrame(index=idx[idx > sidx.max()])
])
result = df2.loc[idx]
with tm.assert_produces_warning(FutureWarning, check_stacklevel=False):
result = df2.loc[idx]
tm.assert_frame_equal(result, expected, check_index_type=False)

def test_iloc_empty_list_indexer_is_ok(self):
Expand Down
18 changes: 12 additions & 6 deletions pandas/tests/indexing/test_indexing.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,8 @@ def test_dups_fancy_indexing(self):
'test1': [7., 6, np.nan],
'other': ['d', 'c', np.nan]}, index=rows)

result = df.loc[rows]
with tm.assert_produces_warning(FutureWarning, check_stacklevel=False):
result = df.loc[rows]
tm.assert_frame_equal(result, expected)

# see GH5553, make sure we use the right indexer
Expand All @@ -186,7 +187,8 @@ def test_dups_fancy_indexing(self):
'other': [np.nan, np.nan, np.nan,
'd', 'c', np.nan]},
index=rows)
result = df.loc[rows]
with tm.assert_produces_warning(FutureWarning, check_stacklevel=False):
result = df.loc[rows]
tm.assert_frame_equal(result, expected)

# inconsistent returns for unique/duplicate indices when values are
Expand All @@ -203,20 +205,23 @@ def test_dups_fancy_indexing(self):

# GH 4619; duplicate indexer with missing label
df = DataFrame({"A": [0, 1, 2]})
result = df.loc[[0, 8, 0]]
with tm.assert_produces_warning(FutureWarning, check_stacklevel=False):
result = df.loc[[0, 8, 0]]
expected = DataFrame({"A": [0, np.nan, 0]}, index=[0, 8, 0])
tm.assert_frame_equal(result, expected, check_index_type=False)

df = DataFrame({"A": list('abc')})
result = df.loc[[0, 8, 0]]
with tm.assert_produces_warning(FutureWarning, check_stacklevel=False):
result = df.loc[[0, 8, 0]]
expected = DataFrame({"A": ['a', np.nan, 'a']}, index=[0, 8, 0])
tm.assert_frame_equal(result, expected, check_index_type=False)

# non unique with non unique selector
df = DataFrame({'test': [5, 7, 9, 11]}, index=['A', 'A', 'B', 'C'])
expected = DataFrame(
{'test': [5, 7, 5, 7, np.nan]}, index=['A', 'A', 'A', 'A', 'E'])
result = df.loc[['A', 'A', 'E']]
with tm.assert_produces_warning(FutureWarning, check_stacklevel=False):
result = df.loc[['A', 'A', 'E']]
tm.assert_frame_equal(result, expected)

# GH 5835
Expand All @@ -227,7 +232,8 @@ def test_dups_fancy_indexing(self):
expected = pd.concat(
[df.loc[:, ['A', 'B']], DataFrame(np.nan, columns=['C'],
index=df.index)], axis=1)
result = df.loc[:, ['A', 'B', 'C']]
with tm.assert_produces_warning(FutureWarning, check_stacklevel=False):
result = df.loc[:, ['A', 'B', 'C']]
tm.assert_frame_equal(result, expected)

# GH 6504, multi-axis indexing
Expand Down
Loading

0 comments on commit 9020827

Please sign in to comment.