Thanks to visit codestin.com
Credit goes to github.com

Skip to content

FIX FunctionTransformer check_input fails for object type input - #19916

Merged
thomasjpfan merged 18 commits into
scikit-learn:mainfrom
MaxwellLZH:fix/function-transformer-string-input
Feb 17, 2022
Merged

FIX FunctionTransformer check_input fails for object type input#19916
thomasjpfan merged 18 commits into
scikit-learn:mainfrom
MaxwellLZH:fix/function-transformer-string-input

Conversation

@MaxwellLZH

Copy link
Copy Markdown
Contributor

Reference Issues/PRs

This is a fix to issue #19905. Now we check if the input is string or object first using X.dtype.kind in ('U', 'S', 'O'). X and X_roundtrip are converted to object type before equality check because for some reason when array.dtype=='U', np.nan equals np.nan (shown in the following snippet)

a = np.array(['abb', 'b', 'd', np.nan])
b = np.array(['abb', 'b', 'd', np.nan])
print(a.dtype.kind)  # U
a == b   # array([ True,  True,  True,  True])

Corresponding test case is also added.

@glemaitre glemaitre 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.

I think that we can limit ourselves to object dtype only but make sure that it works for a list, an array, and a series (and as an extension on a dataframe).

Please add an entry to the change log at doc/whats_new/v1.0.rst. Like the other entries there, please reference this pull request with :pr: and credit yourself (and other contributors if applicable) with :user:.

Comment thread sklearn/preprocessing/tests/test_function_transformer.py Outdated
Comment on lines +109 to +118

if not hasattr(X, 'dtype'):
X = np.asarray(X)
is_object_type = X.dtype.kind in ('U', 'S', 'O')
if is_object_type:
# need to coerce to Object type before elementwise comparison
# because when dtype.kind = 'S', np.nan == np.nan returns True
valid = np.alltrue(X.astype('O') == X_round_trip.astype('O'))
else:
valid = _allclose_dense_sparse(X[idx_selected], X_round_trip)

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 think that we could simplify the code:

        X = np.asarray(X) if not hasattr(X, "dtype") else X

        idx_selected = slice(None, None, max(1, X.shape[0] // 100))
        X_round_trip = self.inverse_transform(self.transform(X[idx_selected]))

        if np.issubdtype(X.dtype, np.number):
            valid = _allclose_dense_sparse(X[idx_selected], X_round_trip)
        else:
            valid = np.all(X == X_round_trip)

@glemaitre glemaitre 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

if np.issubdtype(X.dtype, np.number):
valid = _allclose_dense_sparse(X[idx_selected], X_round_trip)
else:
valid = np.all(X == X_round_trip)

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.

If inverse_func returns a dataframe of strings & floats, this can fail for floats:

import pandas as pd
import numpy as np

X = pd.DataFrame({"a": ["first"], "a_int": [1], "a_float": [0.3]})
X_round_trip = pd.DataFrame({"a": ["first"], "a_int": [1], "a_float": [0.1 + 0.1 + 0.1]})

X_np = np.asarray(X)
np.all(X_np == X_round_trip)  # False

I would prefer not to deal with mixed types when checking for the inverse in FunctionTransformer. I think we can error when X_round_trip is a not a numerical ndarray and suggest check_inverse=False.

WDYT @glemaitre ?

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.

This is probably the best. I would have like to support when we have only strings but it would be difficult to detect. Only the ndarray with "S" or "U" dtype could be easier to handle.

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.

This still needs to be address. I think we should error when X_round_trip is a dataframe that is not all numerical and suggest check_inverse=False.

Checking the inverse with DataFrames with all numerical dtypes works on main, so we need to be careful to continue to support that use case.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I've updated _check_inverse_transform as suggested, now it raises error when input is not all numerical

@glemaitre

Copy link
Copy Markdown
Member

Could you solve the conflicts?


def _check_inverse_transform(self, X):
"""Check that func and inverse_func are the inverse."""
X = np.asarray(X) if not hasattr(X, "dtype") else X

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.

This is backward incompatible because transform (which calls self.func) can be designed to take in DataFrame and calling np.asarray(X) would convert the DataFrame into an ndarray.

(I am assuming that validate is False)

if np.issubdtype(X.dtype, np.number):
valid = _allclose_dense_sparse(X[idx_selected], X_round_trip)
else:
valid = np.all(X == X_round_trip)

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.

This still needs to be address. I think we should error when X_round_trip is a dataframe that is not all numerical and suggest check_inverse=False.

Checking the inverse with DataFrames with all numerical dtypes works on main, so we need to be careful to continue to support that use case.

@glemaitre

Copy link
Copy Markdown
Member

The documentation CI is still failing. Could you solve this before I take a new look at it?

Comment thread doc/whats_new/v1.0.rst Outdated
`n_features_in_` and will be removed in 1.2. :pr:`20240` by
:user:`Jérémie du Boisberranger <jeremiedbb>`.

- |Fix| :meth:`preprocessing.FunctionTransformer._check_inverse_transform`

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.

You need to move this in 1.1 and check the warning raised by the documentation CI

Comment on lines +160 to +168
X = np.asarray(X) if not hasattr(X, "dtype") else X
if np.issubdtype(X.dtype, np.number):
valid = _allclose_dense_sparse(X[idx_selected], X_round_trip)
else:
raise ValueError(
"'check_inverse' is only supported when all the elements in `X` is"
" numerical."
)
if not valid:

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 think we can early exit here:

        if not np.issubdtype(X.dtype, np.number):
            raise ValueError(
                "'check_inverse' is only supported when all the elements in `X` is"
                " numerical."
            )

        if not _allclose_dense_sparse(X[idx_selected], X_round_trip):
            warnings.warn(
                "The provided functions are not strictly"
                " inverse of each other. If you are sure you"
                " want to proceed regardless, set"
                " 'check_inverse=False'.",
                UserWarning,
            )

Also we do not need to cast since this function already assumes that X is an array or sparse. (There are calls to X.shape[0] and at the beginning of _check_inverse_transform.

@thomasjpfan thomasjpfan 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.

Minor nit about the changelog otherwise LGTM

Comment thread doc/whats_new/v1.1.rst Outdated
@thomasjpfan
thomasjpfan merged commit dbcd4d5 into scikit-learn:main Feb 17, 2022
thomasjpfan added a commit to thomasjpfan/scikit-learn that referenced this pull request Mar 1, 2022
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.

4 participants