Skip to content

API: query / boolean selection with nullable dtypes with NAs #31503

Closed
@tdpetrou

Description

@tdpetrou

Code Sample

Create a dataframe with nullable integer, string, and float data types.

>>> df = df = pd.DataFrame({'a': [1, 3, np.nan], 'b': ['rain', 'shine', None], 
                                                    'a_float': [1.1, 3.3, np.nan]})
>>> df = df.convert_dtypes()
>>> df
a b a_float
0 1 rain 1.1
1 3 shine 3.3
2 <NA> <NA> nan

Verify data types and attempt to use query

>>> df.dtypes
a            Int64
b            string
a_float      float64

>>> df.query('a > 2') # same as df[df['a'] > 2]
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
~/Documents/Code Practice/pandas-dev/pandas/pandas/core/frame.py in query(self, expr, inplace, **kwargs)
   3227         try:
-> 3228             new_data = self.loc[res]
   3229         except ValueError:

~/Documents/Code Practice/pandas-dev/pandas/pandas/core/indexing.py in __getitem__(self, key)
   1683             maybe_callable = com.apply_if_callable(key, self.obj)
-> 1684             return self._getitem_axis(maybe_callable, axis=axis)
   1685 

~/Documents/Code Practice/pandas-dev/pandas/pandas/core/indexing.py in _getitem_axis(self, key, axis)
   1798             return self._get_slice_axis(key, axis=axis)
-> 1799         elif com.is_bool_indexer(key):
   1800             return self._getbool_axis(key, axis=axis)

~/Documents/Code Practice/pandas-dev/pandas/pandas/core/common.py in is_bool_indexer(key)
    133                 if np.any(key.isna()):
--> 134                     raise ValueError(na_msg)
    135             return True

ValueError: cannot mask with array containing NA / NaN values

During handling of the above exception, another exception occurred:

ValueError                                Traceback (most recent call last)
<ipython-input-52-e5d239635d7b> in <module>
----> 1 df.query('a > 2')

~/Documents/Code Practice/pandas-dev/pandas/pandas/core/frame.py in query(self, expr, inplace, **kwargs)
   3230             # when res is multi-dimensional loc raises, but this is sometimes a
   3231             # valid query
-> 3232             new_data = self[res]
   3233 
   3234         if inplace:

~/Documents/Code Practice/pandas-dev/pandas/pandas/core/frame.py in __getitem__(self, key)
   2784 
   2785         # Do we have a (boolean) 1d indexer?
-> 2786         if com.is_bool_indexer(key):
   2787             return self._getitem_bool_array(key)
   2788 

~/Documents/Code Practice/pandas-dev/pandas/pandas/core/common.py in is_bool_indexer(key)
    132             if is_extension_array_dtype(key.dtype):
    133                 if np.any(key.isna()):
--> 134                     raise ValueError(na_msg)
    135             return True
    136     elif isinstance(key, list):

ValueError: cannot mask with array containing NA / NaN values

>>> df.query('a_float > 2')
a b a_float
1 3 shine 3.3

Using query with strings works...

>>> df.query('b == "rain"')
a b a_float
0 1 rain 1.1

...but fails for boolean selection

>>> df[df['b'] == 'rain']
ValueError: cannot mask with array containing NA / NaN values

strings also fail for inequalities

>>> df.query('b >= "rain"') # also df.query('b > "rain"')
ValueError: cannot mask with array containing NA / NaN values

Problem description

The query method behaves differently for nullable integers, strings, and floats. Here's my summary of how I think they work with the query method assuming there are missing values in the columns.

  • nullable integers - fails
  • strings - works with equality, fails with inequality
  • float - works for all

I find it extremely difficult to use if the behavior for all of these types are different for query and boolean selection.

Expected Output

I think I would prefer to have both query and boolean selection working like they do with floats, where missing values evaluate as False in a condition. And even if there are missing values in the boolen mask itself, treat those as False. This would harmonize the behavior for all data types.

This would leave it up to the user to check for missing values. I believe SQL where clauses work in such a manner (missing values in conditions evaluate as False).

Output of pd.show_versions()

INSTALLED VERSIONS

commit : None
python : 3.8.1.final.0
python-bits : 64
OS : Darwin
OS-release : 19.2.0
machine : x86_64
processor : i386
byteorder : little
LC_ALL : None
LANG : en_US.UTF-8
LOCALE : en_US.UTF-8

pandas : 0+untagged.1.gce8af21.dirty
numpy : 1.18.1
pytz : 2019.3
dateutil : 2.8.1
pip : 20.0.2
setuptools : 45.1.0.post20200127
Cython : 0.29.14
pytest : None
hypothesis : None
sphinx : None
blosc : None
feather : None
xlsxwriter : None
lxml.etree : 4.5.0
html5lib : None
pymysql : None
psycopg2 : None
jinja2 : 2.10.3
IPython : 7.11.1
pandas_datareader: None
bs4 : None
bottleneck : None
fastparquet : None
gcsfs : None
lxml.etree : 4.5.0
matplotlib : 3.1.1
numexpr : None
odfpy : None
openpyxl : None
pandas_gbq : None
pyarrow : None
pytables : None
pytest : None
pyxlsb : None
s3fs : None
scipy : 1.3.1
sqlalchemy : 1.3.13
tables : None
tabulate : 0.8.3
xarray : None
xlrd : None
xlwt : None
xlsxwriter : None
numba : None

Metadata

Metadata

Assignees

No one assigned

    Labels

    API DesignMissing-datanp.nan, pd.NaT, pd.NA, dropna, isnull, interpolateNA - MaskedArraysRelated to pd.NA and nullable extension arrays

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions