Booleans and labels

Contents

Booleans and labels#

We have already used Boolean series to index data frames and other series.

This page gives a little more details about how that works, in order to explain some subtleties in results.

We return to the World Bank statistics on development and gender in gender_stats.csv.

Download that file to the same directory as this notebook, if you are running on your own computer.

Here we pull out the rows for the countries with the highest Gross Domestic Product. This may be familiar from the data frame intro and the Pandas indexing page.

import numpy as np
import pandas as pd
# Safe setting for Pandas.  Needs Pandas version >= 1.5.
pd.set_option('mode.copy_on_write', True)
# Load the data file as a data frame.
gender_data = pd.read_csv('gender_stats.csv')

# Sort by GDP.
df_by_gdp = gender_data.sort_values('gdp_us_billion', ascending=False)
# Take the top 5 rows.
richest_5 = df_by_gdp.head(5)
richest_5
country_name country_code fert_rate gdp_us_billion health_exp_per_cap health_exp_pub prim_ed_girls mat_mort_ratio population
202 United States USA 1.860875 17369.124600 9060.068657 8.121961 48.758830 14.00 318.558175
35 China CHN 1.558750 10182.790479 657.748859 3.015530 46.297964 28.75 1364.446000
97 Japan JPN 1.430000 5106.024760 3687.126279 8.496074 48.744199 5.75 127.297102
49 Germany DEU 1.450000 3601.226158 4909.659884 8.542615 48.568695 6.25 81.281645
67 United Kingdom GBR 1.842500 2768.864417 3357.983675 7.720655 48.791809 9.25 64.641557

Consider the index (row labels) of this 5-row data frame:

richest_5.index
Index([202, 35, 97, 49, 67], dtype='int64')

Now let us say we want to select some of these rows with Boolean indexing. Here is a Boolean series with True for rows where health_exp_per_cap is greater than 3500 dollars, False otherwise.

# Create a Boolean series with True for big spender rows, False otherwise.
is_big_spender = richest_5['health_exp_per_cap'] > 3500
is_big_spender
202     True
35     False
97      True
49      True
67     False
Name: health_exp_per_cap, dtype: bool

Notice that this Series, like all Series, has an index, which is the same as the index from the Series we used to make it (richest_5['health_exp_per_cap']), and therefore, the same as the index for the data frame (richest_5).

is_big_spender.index
Index([202, 35, 97, 49, 67], dtype='int64')

Let’s say we are interested in the country_name for countries spending more than 3500 dollars per capita on health care. We can do this with, for example, loc:

richest_5.loc[is_big_spender, 'country_name']
202    United States
97             Japan
49           Germany
Name: country_name, dtype: object

loc gives us the rows for which corresponding elements in is_big_spender are True.

But what does corresponding mean? Does it mean corresponding in terms of position? Or in terms of index (row / element labels)?

Here’s the Boolean series again:

is_big_spender
202     True
35     False
97      True
49      True
67     False
Name: health_exp_per_cap, dtype: bool

If corresponding means corresponding in terms of position then we would take the first row of richest_5 (because the first element in is_big_spender is True) but not the second row (because the second element in is_big_spender is False) and so on.

If corresponding means corresponding in terms of labels then we would take the row from richest_5 with label 202 (because the element labeled 202 in is_big_spender is True), but we do not take the row labeled 35 (because the element labeled 35 in is_big_spender is False), and so on.

We cannot distinguish between these alternatives at the moment, because the order of the labels is the same in the data frame richest_5 and the series is_big_spender, so positions and labels will give the same answer.

We can distinguish when the order of the labels is not the same in the Boolean series and the data frame. For example, we can sort the series, like this:

sorted_is_big_spender = is_big_spender.sort_values()
sorted_is_big_spender
35     False
67     False
202     True
97      True
49      True
Name: health_exp_per_cap, dtype: bool

Now the order of the row labels is different in the series (above) and the data frame (below):

richest_5.index
Index([202, 35, 97, 49, 67], dtype='int64')

If we index with this sorted series, and indexing uses the position of the True and False values, we would expect to see the last three rows, because the values in the last three positions are True (and the others are False).

If indexing uses labels, then we expect to get the same answer we got before, because the relationship between the True / False values and labels hasn’t changed - the elements labeled 202, 97 and 49 have True values, so we expect to see those rows in the result. So (drum roll):

richest_5.loc[sorted_is_big_spender, 'country_name']
202    United States
97             Japan
49           Germany
Name: country_name, dtype: object

The result is the same as for the not-sorted series - and we have found that loc indexing uses the labels rather than the positions when indexing with a Boolean series.

On reflection, maybe that is not surprising - after all, we already know that loc indexing does indexing by label.

We usually will not have to worry about the difference between labels and positions in Boolean Series, because, usually, the row labels and positions are the same in the thing we index (data frame, or Series) and the Boolean series we use for indexing. This was the case at the top of this page, and in all previous times you have seen Boolean indexing on data frames or series.

What about iloc?#

iloc does indexing by position - so what approach does iloc take to the labels on a Boolean series?

We guess that iloc does not use the labels on a Boolean series, but what does it do? Does it throw away the labels and just look at the positions of the True and False values? Or something else?

# iloc indexing with a Boolean series.
# The 'country_name' column is the first column,
# so it is at offset 0 in the columns.
richest_5.iloc[sorted_is_big_spender, 0]
---------------------------------------------------------------------------
NotImplementedError                       Traceback (most recent call last)
Cell In[11], line 4
      1 # iloc indexing with a Boolean series.
      2 # The 'country_name' column is the first column,
      3 # so it is at offset 0 in the columns.
----> 4 richest_5.iloc[sorted_is_big_spender, 0]

File /opt/hostedtoolcache/Python/3.11.9/x64/lib/python3.11/site-packages/pandas/core/indexing.py:1184, in _LocationIndexer.__getitem__(self, key)
   1182     if self._is_scalar_access(key):
   1183         return self.obj._get_value(*key, takeable=self._takeable)
-> 1184     return self._getitem_tuple(key)
   1185 else:
   1186     # we by definition only have the 0th axis
   1187     axis = self.axis or 0

File /opt/hostedtoolcache/Python/3.11.9/x64/lib/python3.11/site-packages/pandas/core/indexing.py:1690, in _iLocIndexer._getitem_tuple(self, tup)
   1689 def _getitem_tuple(self, tup: tuple):
-> 1690     tup = self._validate_tuple_indexer(tup)
   1691     with suppress(IndexingError):
   1692         return self._getitem_lowerdim(tup)

File /opt/hostedtoolcache/Python/3.11.9/x64/lib/python3.11/site-packages/pandas/core/indexing.py:966, in _LocationIndexer._validate_tuple_indexer(self, key)
    964 for i, k in enumerate(key):
    965     try:
--> 966         self._validate_key(k, i)
    967     except ValueError as err:
    968         raise ValueError(
    969             "Location based indexing can only have "
    970             f"[{self._valid_types}] types"
    971         ) from err

File /opt/hostedtoolcache/Python/3.11.9/x64/lib/python3.11/site-packages/pandas/core/indexing.py:1578, in _iLocIndexer._validate_key(self, key, axis)
   1576 if hasattr(key, "index") and isinstance(key.index, Index):
   1577     if key.index.inferred_type == "integer":
-> 1578         raise NotImplementedError(
   1579             "iLocation based boolean "
   1580             "indexing on an integer type "
   1581             "is not available"
   1582         )
   1583     raise ValueError(
   1584         "iLocation based boolean indexing cannot use "
   1585         "an indexable as a mask"
   1586     )
   1587 return

NotImplementedError: iLocation based boolean indexing on an integer type is not available

It does something else - it gives an error, telling us that it will not use the index. This is so whatever the index order - even if the index and the positions give the same answer:

# iloc with the original Boolean series, where positions
# and labels match.
richest_5.iloc[is_big_spender, 0]
---------------------------------------------------------------------------
NotImplementedError                       Traceback (most recent call last)
Cell In[12], line 3
      1 # iloc with the original Boolean series, where positions
      2 # and labels match.
----> 3 richest_5.iloc[is_big_spender, 0]

File /opt/hostedtoolcache/Python/3.11.9/x64/lib/python3.11/site-packages/pandas/core/indexing.py:1184, in _LocationIndexer.__getitem__(self, key)
   1182     if self._is_scalar_access(key):
   1183         return self.obj._get_value(*key, takeable=self._takeable)
-> 1184     return self._getitem_tuple(key)
   1185 else:
   1186     # we by definition only have the 0th axis
   1187     axis = self.axis or 0

File /opt/hostedtoolcache/Python/3.11.9/x64/lib/python3.11/site-packages/pandas/core/indexing.py:1690, in _iLocIndexer._getitem_tuple(self, tup)
   1689 def _getitem_tuple(self, tup: tuple):
-> 1690     tup = self._validate_tuple_indexer(tup)
   1691     with suppress(IndexingError):
   1692         return self._getitem_lowerdim(tup)

File /opt/hostedtoolcache/Python/3.11.9/x64/lib/python3.11/site-packages/pandas/core/indexing.py:966, in _LocationIndexer._validate_tuple_indexer(self, key)
    964 for i, k in enumerate(key):
    965     try:
--> 966         self._validate_key(k, i)
    967     except ValueError as err:
    968         raise ValueError(
    969             "Location based indexing can only have "
    970             f"[{self._valid_types}] types"
    971         ) from err

File /opt/hostedtoolcache/Python/3.11.9/x64/lib/python3.11/site-packages/pandas/core/indexing.py:1578, in _iLocIndexer._validate_key(self, key, axis)
   1576 if hasattr(key, "index") and isinstance(key.index, Index):
   1577     if key.index.inferred_type == "integer":
-> 1578         raise NotImplementedError(
   1579             "iLocation based boolean "
   1580             "indexing on an integer type "
   1581             "is not available"
   1582         )
   1583     raise ValueError(
   1584         "iLocation based boolean indexing cannot use "
   1585         "an indexable as a mask"
   1586     )
   1587 return

NotImplementedError: iLocation based boolean indexing on an integer type is not available

If you want to use Boolean indexing with iloc, you can use a Boolean sequence without row / element labels, such as a Numpy array. We can achieve that by converting the Boolean Series to a Numpy array, and therefore, throwing away the index (element labels):

sorted_is_big_spender_arr = np.array(sorted_is_big_spender)
sorted_is_big_spender_arr
array([False, False,  True,  True,  True])

This Boolean sequence will work with iloc, because it has no index (element labels) to trip it up:

# You can use a Boolean array to index rows, with "iloc".
richest_5.iloc[sorted_is_big_spender_arr, 0]
97             Japan
49           Germany
67    United Kingdom
Name: country_name, dtype: object

Notice this does gives us the rows where the matching position has True.