Skip to content

ENH Adds pandas IntegerArray support to check_array - #16508

Merged
ogrisel merged 24 commits into
scikit-learn:masterfrom
thomasjpfan:pandas_pd_na_support
Apr 28, 2020
Merged

ogrisel merged 24 commits into
scikit-learn:masterfrom
thomasjpfan:pandas_pd_na_support

Conversation

@thomasjpfan

@thomasjpfan thomasjpfan commented Feb 21, 2020 •

Copy link
Copy Markdown
Member

Reference Issues/PRs

Resolves #16498

What does this implement/fix? Explain your changes.

Uses the name of the dtype to check if the dtype is a pandas IntegerArray.

Any other comments?

The __array__ interface for panda's IntegerArray results in a object:

import pandas as pd
X = pd.Series([1, 2, pd.NA])

np.asarray(X)
# array([1, 2, <NA>], dtype=object)

np.asarray(X, dtype=float)
# ValueError

np.asarray(X.astype(float))
# array([ 1.,  2., nan])

Comment thread sklearn/preprocessing/tests/test_common.py Outdated
Comment thread sklearn/utils/tests/test_validation.py Outdated
@adrinjalali

adrinjalali commented Apr 21, 2020 •

Copy link
Copy Markdown
Member

@TomAugspurger could you please maybe have a look at this one?

@TomAugspurger

Copy link
Copy Markdown
Contributor

Looks reasonable.

Just to confirm: the desired behavior is to coerce NA to NaN when force_all_finite=True? You're comfortable with the precision loss for large integers that comes with that?

Comment thread sklearn/utils/validation.py Outdated
Comment on lines +474 to +475
if isinstance(dtype_iter, (Int8Dtype, Int16Dtype,
Int32Dtype, Int64Dtype)):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We also have unsigned variants. Is that relevant anywhere in check_array?

Comment thread sklearn/utils/validation.py Outdated
Comment on lines +523 to +524
# convert dataframe with Integer extension arrays with floats
array = array.astype(None)

@TomAugspurger TomAugspurger Apr 22, 2020 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suspect that IntegerArray.astype(None) returning a float64 ndarray isn't deliberate, right @jorisvandenbossche?.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that is deliberate, indeed (I would say that's a bug .. although it seems consistent with numpy where np.dtype(None) is float64).

@thomasjpfan

Copy link
Copy Markdown
Member Author

Just to confirm: the desired behavior is to coerce NA to NaN when force_all_finite=True?

It is to corece when allow_all_finite is False or 'allow-nan'.

You're comfortable with the precision loss for large integers that comes with that?

It is a compromise to trying to push a extension arrays into a homogeneous numpy array, while making it easier for uses to use pandas + scikit-learn. I am slightly uncomfortable with this and am open to an another approach.

This PR is using the following to cohere:

df = pd.DataFrame(
    {'a': pd.Series([1, 2, 3, None], dtype="UInt8"),
     'b': pd.Series([1, 4, 5, 10], dtype="int")})

df.dtypes
# a    UInt8
# b    int64
# dtype: object

# converst pd.NA to np.nan
df_other = df.astype(None)

df_other.dtype
# a    float64
# b    float64
# dtype: object

which allows us to call asarray and get a numpy array. I do not think this is documented behavior. I can try another approach if this implementation is not officially support by pandas.

Comment thread sklearn/utils/validation.py Outdated

if has_pd_interger_array:
# If there are any pandas integer extension arrays,
array = array.astype(None)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think .astype(np.dtype('float64')) would be best (assuming you always want 64-bit here).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or to_numpy()? That's even more explicit (and certainly given that astype behaviour might still change, eg to always return new ExtensionArray), although only available for more recent versions? (which might not be a problem, since this is still experimental anyway)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at the actual code now, array here will typically be a DataFrame (in case of pandas), and it seemt that for DataFrame.to_numpy we didn't add the na_value keyword .. (that's a pity, as I think df.to_numpy(dtype='float64', na_value=np.nan) would have been the most robust going forward).

@jorisvandenbossche

Copy link
Copy Markdown
Member

You're comfortable with the precision loss for large integers that comes with that?

Note that you can already have this problem with dataframes with the standard numpy dtypes if you have eg a mix of float and int columns (which will all get converted to a single float array)

@adrinjalali adrinjalali left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

codecov complaints seem unrelated. LGTM, thanks @thomasjpfan

@jnothman jnothman left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add to change log, please.

What's the story with imputers?

Comment thread sklearn/utils/validation.py Outdated
Comment thread sklearn/utils/tests/test_validation.py Outdated
@jnothman

Copy link
Copy Markdown
Member

What's the story with imputers?

@ogrisel ogrisel left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM besides the following comments.

Please also update the docstring of force_all_finite to explicitly mention that pd.NA is accepted and will be converted as np.nan values in a np.float64 arrays (don't forget the versionchanged directive).

Comment thread sklearn/utils/validation.py Outdated
Int32Dtype, Int64Dtype,
UInt8Dtype, UInt16Dtype,
UInt32Dtype, UInt64Dtype)):
has_pd_interger_array = True

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
has_pd_interger_array = True
has_pd_integer_array = True

Comment thread sklearn/utils/validation.py Outdated
if all(isinstance(dtype, np.dtype) for dtype in dtypes_orig):
dtype_orig = np.result_type(*dtypes_orig)

if has_pd_interger_array:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if has_pd_interger_array:
if has_pd_integer_array:

Comment thread sklearn/utils/validation.py Outdated
"It will be converted to a dense numpy array."
)

has_pd_interger_array = False

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
has_pd_interger_array = False
has_pd_integer_array = False

Comment thread doc/whats_new/v0.23.rst Outdated
:pr:`16728` by `Thomas Fan`_.

- |Enhancement| :func:`utils.validation.check_array` supports pandas
IntegerArray. :pr:`16508` by `Thomas Fan`_.

@ogrisel ogrisel Apr 27, 2020 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this could be a bit more descriptive: utils.validation.check_array accepts pandas IntegerArray when force_all_finite is set to False or allow-nan in which case the data is converted to floating point values with pd.NA values replaced by np.nan.

Maybe you could also mention that, as a consequence, all sklearn.preprocessing transformers (also known as "scalers") that accept numeric inputs with missing values represented as np.nan now also accept being directly fed with pandas dataframes with pandas.Int* or pandas.UInt*- typed columns that use pandas.NA as the missing value marker.

@ogrisel

ogrisel commented Apr 27, 2020

Copy link
Copy Markdown
Member

What's the story with imputers?

They might work as a result of this PR but we need to update their own tests and the changelog should also mention them.

I believe the HistGradientBoosting* estimators should now also accept dataframes with IntegerArray columns.

@ogrisel ogrisel left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few more comments:

  • the docstring of other functions method that expose the force_all_finite option in the public API might also need to be updated similarly:
$ git grep "force_all_finite :"
sklearn/metrics/pairwise.py:    force_all_finite : boolean or 'allow-nan', (default=True)
sklearn/metrics/pairwise.py:    force_all_finite : boolean or 'allow-nan', (default=True)
sklearn/utils/validation.py:    force_all_finite : boolean or 'allow-nan', (default=True)
sklearn/utils/validation.py:    force_all_finite : boolean or 'allow-nan', (default=True)
sklearn/utils/validation.py:    force_all_finite : boolean or 'allow-nan', (default=True)
sklearn/utils/validation.py:    force_all_finite : boolean or 'allow-nan', (default=True)
  • we need to extend the test to test for the resulting dtype of this conversion:

Comment thread sklearn/utils/tests/test_validation.py Outdated
@thomasjpfan

Copy link
Copy Markdown
Member Author

Added tests to sklearn/impute/tests/test_common.py to test the new functionality with imputers. This can be a little strange because missing_value=np.nan, but the missing value of the input is pd.Na, because we are converting them to np.nan.

@ogrisel

ogrisel commented Apr 27, 2020

Copy link
Copy Markdown
Member

Added tests to sklearn/impute/tests/test_common.py to test the new functionality with imputers. This can be a little strange because missing_value=np.nan, but the missing value of the input is pd.Na, because we are converting them to np.nan.

Maybe we should allow the users to pass missing_value=pd.Na in imputers? Or maybe we should just mention that pd.NA is automatically converted to np.nan in the docstring of the missing_value parameter?

@ogrisel

ogrisel commented Apr 28, 2020

Copy link
Copy Markdown
Member

There are new sphinx warnings in this PR:

Sphinx Warnings in affected files

    sklearn/impute/_base.py:docstring of sklearn.impute.MissingIndicator:15: WARNING: Inline interpreted text or phrase reference start-string without end-string.
    sklearn/impute/_base.py:docstring of sklearn.impute.SimpleImputer:13: WARNING: Inline interpreted text or phrase reference start-string without end-string.
    sklearn/impute/_iterative.py:docstring of sklearn.impute.IterativeImputer:30: WARNING: Inline interpreted text or phrase reference start-string without end-string.
    sklearn/impute/_knn.py:docstring of sklearn.impute.KNNImputer:15: WARNING: Inline interpreted text or phrase reference start-string without end-string. 

@ogrisel ogrisel left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo that breaks sphinx:

Comment thread sklearn/impute/_base.py Outdated
Comment thread sklearn/impute/_iterative.py Outdated
Comment thread sklearn/impute/_knn.py Outdated
Comment thread sklearn/impute/_base.py Outdated
Comment thread doc/whats_new/v0.23.rst Outdated
for each feature. :pr:`16403` by :user:`Narendra Mukherjee <narendramukherjee>`.

- |Enhancement| :class:`impute.SimpleImputer`, :class:`impute.KNNImputer`, and
:class:`impute.SimpleImputer` accepts pandas IntegerArray with nan values.

@jorisvandenbossche jorisvandenbossche Apr 28, 2020 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
:class:`impute.SimpleImputer` accepts pandas IntegerArray with nan values.
:class:`impute.SimpleImputer` accepts pandas' nullable integer dtype with missing values.

Just a suggestion, but in general we try to speak about the dtype, as "IntegerArray" is only used under the hood and not directly visible to most users (unless you access the underlying values of a single column), and thus not necessarily a term that users are familiar with

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(but it makes it more verbose, though, ..)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jorisvandenbossche that's a good point. There are other occurrence of the "IntegerArray" in this PR. I think we should covert them all to "pandas' nullable integer dtype/values/column/dataframe" depending on the context.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@thomasjpfan if you agree with this proposal, I can give it a shot.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated the PR with removing the "IntegerArray" wording in place of "pandas' nullable ..."

@jorisvandenbossche

Copy link
Copy Markdown
Member

General comment: although integer columns will certainly be the most common use case for scikit-learn, pd.NA is not only used for nullable integer columns in pandas, but also for the dedicated string data type and for the nullable boolean dtype. So we might want to consider more general handling of this not specific to integers in the future.

@ogrisel

ogrisel commented Apr 28, 2020

Copy link
Copy Markdown
Member

So we might want to consider more general handling of this not specific to integers in the future.

In particular for categorical encoders with missing values but this is technically a very different and complementary change, I believe.

@ogrisel
ogrisel merged commit acbe13c into scikit-learn:master Apr 28, 2020
@ogrisel

ogrisel commented Apr 28, 2020

Copy link
Copy Markdown
Member

Merged!

@thomasjpfan

Copy link
Copy Markdown
Member Author

#17010

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add support pd.Na in preprocessing module

7 participants