FIX FunctionTransformer check_input fails for object type input - #19916
Conversation
glemaitre
left a comment
There was a problem hiding this comment.
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:.
|
|
||
| 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) |
There was a problem hiding this comment.
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)| 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) |
There was a problem hiding this comment.
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) # FalseI 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 ?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
I've updated _check_inverse_transform as suggested, now it raises error when input is not all numerical
|
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 |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
|
The documentation CI is still failing. Could you solve this before I take a new look at it? |
| `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` |
There was a problem hiding this comment.
You need to move this in 1.1 and check the warning raised by the documentation CI
| 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: |
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
Minor nit about the changelog otherwise LGTM
Co-authored-by: Thomas J. Fan <[email protected]>
…it-learn#19916) Co-authored-by: Guillaume Lemaitre <[email protected]> Co-authored-by: Thomas J. Fan <[email protected]>
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').XandX_roundtripare converted to object type before equality check because for some reason when array.dtype=='U',np.nanequalsnp.nan(shown in the following snippet)Corresponding test case is also added.