-
Notifications
You must be signed in to change notification settings - Fork 83
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add nullable type incompatibility properties to the components that have them #4031
Conversation
Codecov Report
@@ Coverage Diff @@
## main #4031 +/- ##
=======================================
+ Coverage 99.7% 99.7% +0.1%
=======================================
Files 349 349
Lines 37257 37472 +215
=======================================
+ Hits 37136 37354 +218
+ Misses 121 118 -3
Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here. |
subset_cols=X_schema._filter_cols( | ||
exclude=["IntegerNullable", "BooleanNullable"], | ||
exclude=["IntegerNullable", "BooleanNullable", "AgeNullable"], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We'll likely remove this logic once we integrate the new nullable type handling into AutoMLSearch, but I wanted to add in AgeNullable
here so that we could test with it now.
The one reason we may keep is is that nans are present at this stage, so we'll have to convert types to Double
and Categorical
, and we may want logic to not keep those types once nans are gone and we can use Integer
and Boolean
. I'll put more thought into this when I get to #3999
evalml_arima.fit(X_train, y_train) | ||
|
||
# Confirm that the handle nullable types method fixes the error for AutoARIMA | ||
X_train_d, y_train_d = evalml_arima._handle_nullable_types(X_train, y_train) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Once we integrate the new handling into automl search, this call to _handle_nullable_types
will be inside fit and predict, so we'll remove this. Keeping the error checking for sk_arima will help us know when sktime adds support for integer nullable on arima.
["BooleanNullable", "IntegerNullable", "AgeNullable"], | ||
) | ||
# --> this is 180 tests - is it overkill? | ||
def test_components_support_nullable_types( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like we want the tests for making sure our compatible components are actually compatible to be exhaustive. This does add ~180 tests, though, so I just want to make sure there's visibility around that.
|
||
with pytest.raises(Exception): | ||
comp.fit(X, y) | ||
comp.predict(X) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because we are using the actaul evalml components here, this will have to change once we integrate into automl search to call the underlying component_obj
so that we can track when support gets added for that component.
"Unknown label type: 'unknown'", | ||
), | ||
): | ||
oversampler.transform(X, y) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
similar to the test_estimators.py
test - we are using the actual evalml component here, so this will have to change once we integrate into automl search
if has_nans: | ||
y = pd.Series([1, 0, pd.NA, 1, 0] * 4) | ||
else: | ||
y = pd.Series([1, 0, 1, 1, 0] * 4) | ||
y = pd.Series([1, 0, 1, 1, 1] * 4) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed so I could test the oversampler better :)
5580611
to
9735b95
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM - just one clarifying question
X.ww.set_types(logical_types={"feature": "IntegerNullable"}) | ||
X.ww["bool col"] = bool_col | ||
else: | ||
y = nullable_type_target(ltype=nullable_y_ltype, has_nans=False) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why don't we run y = nullable_type_target(ltype=nullable_y_ltype, has_nans=False)
for components that require the time index?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I probably could; I'd just need to change nullable_type_target
to be the correct length for ts_data
and also update it to have the same datetime index values as X
. I felt like it was simpler to just create bool_col
and set y to be it.
9735b95
to
9e72874
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks pretty good! Mostly just test cleanups and clarifying questions, but nothing blocking.
@@ -194,3 +199,21 @@ def transform(self, X, y=None): | |||
y_imputed = ww.init_series(y_imputed) | |||
|
|||
return X_not_all_null, y_imputed | |||
|
|||
def _handle_nullable_types(self, X=None, y=None): | |||
"""Transforms X and y to remove any incompatible nullable types for the time series imputer when the interpolate method is used. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need to treat interpolation differently than the other fill methods? And will this remove any necessary handling for the other methods?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
interpolation is the only one that has any incompatibilities with nullable types, so there's no need to remove nullable types for the other fill methods.
evalml/pipelines/components/transformers/imputers/time_series_imputer.py
Outdated
Show resolved
Hide resolved
@@ -558,3 +560,101 @@ def test_imputer_woodwork_custom_overrides_returned_by_components( | |||
transformed.ww.logical_types["categorical with nan"] | |||
== X.ww.logical_types["categorical with nan"] | |||
) | |||
|
|||
|
|||
def test_imputer_nullable_handling_numeric_interpolate(nullable_type_test_data): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not convinced that these tests are how we want to be testing the TimeSeriesImputer._handle_nullable_types
. It feels like too much is testing sklearn's interpolate, rather than our imputation. I do see why you did it, since just doing the (running the component fails -> calling _handle_nullable_types fixes it) check of other estimators doesn't work here. I think it would be better to combine this test and the one below into one, and focus on testing that the logical splits to only handle nullable types in the interpolation case works as intended. We already have tests ensuring that the super()._handle_nullable_types
has the desired behavior.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a fair point, and I recognize that we do not generally want to be testing our dependencies' behaviors. Part of the goal of this, beyond confirming that our handling lets us avoid the problem, is that this check will notify us when support is added. I have a similar check in the arima tests, and am planning to eventually have them for all incompatibilities.
This is just my preference for how to keep track of our dependencies adding support, though. I recognize it means that when a pandas version is released with support interpolating nullable types, for example, we'll get an error that we will need to handle before we can merge any dependency updates.
That will be a bit of a pain, but it allows us to remove this handling when possible without having to manually keep track of when dependencies add support, which I would argue is more of a pain. Happy to open this up to a larger discussion with the rest of the team.
["BooleanNullable", "IntegerNullable", "AgeNullable"], | ||
) | ||
@pytest.mark.xfail(strict=True, raises=ValueError) | ||
def test_time_series_imputer_nullable_type_incompatibility( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@chukarsten @eccabay @jeremyliweishih I'm still adding the rest of the xfail tests but wanted to run this by yall.
- I have the xfail set to only expect
ValueError
, but unfortunately, we can't specify the expected message. Would people prefer I get as specific about what the expected message is? My thought here is just that I feel like being able to use this format of xfailing where we write code that errors and say that the code is expected to error feels more true to our goal here than triggering the xfail from the test directly when catch the error ourselves. - The
strict
parameter means that our test suite will fail when tests start passing - otherwise they just show asXPASS
which I think would be easy to miss. But if we want to avoid blocking the update checker, we could set this to False and just periodically remember to check. I think that's probably a halfway point between what I'm suggesting and the fully manual checking process.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method of xfail seems like the best to me!
I'm a fan of keeping strict=True
. It's really easy to miss an XPASS
, and we don't have any way of flagging them at this point. I'm pretty sure we already have a bunch of XPASS
ing tests that we might want to revisit...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sliiight change so that we can test that our _handle_nullable_types
fixes the incompatibility - we will expect tests to pass when we handle incompatibilities and xfail other times
@pytest.mark.parametrize(
"nullable_ltype",
["BooleanNullable", "IntegerNullable", "AgeNullable"],
)
@pytest.mark.parametrize(
"handle_incompatibility",
[
True,
pytest.param(
False,
marks=pytest.mark.xfail(strict=True, raises=ValueError),
),
],
)
def test_time_series_imputer_nullable_type_incompatibility(
nullable_type_target,
handle_incompatibility,
nullable_ltype,
):
"""Testing that the nullable type incompatibility that caused us to add handling for the time series imputer
is still present in pandas' interpolate method. If this test is causing the test suite to fail
because the code below no longer raises the expected ValueError, we should confirm that the nullable
types now work for our use case and remove the nullable type handling logic from TimeSeriesImputer."""
nullable_series = nullable_type_target(ltype=nullable_ltype, has_nans=True)
if handle_incompatibility:
imputer = TimeSeriesImputer(target_impute_strategy="interpolate")
imputer.fit(pd.DataFrame(), nullable_series)
_, nullable_series = imputer._handle_nullable_types(None, nullable_series)
nullable_series.interpolate()
# since the category dtype also has incompatibilities with linear interpolate, which is expected | ||
# --> i think this is essentially what happens now but won't that make floating point values that cant be turned back into bools? | ||
if isinstance(y.ww.logical_type, BooleanNullable): | ||
y = ww.init_series(y, Double) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Noticed that interpolate()
cannot handle category
dtypes either. But this, I think, is by design, as the default interpolation is "linear" which doesnt make sense for non numeric values. This is problematic if we try to turn BooleanNullable into Categorical ltype when nans are present, so I'm intercepting it and converting to Double
, which essentially the current behavior.
In the long run, though, I think we don't want to be doing linear interpolation on boolean columns ever - we don't want to end up with 1.5
in a column of all 1s and 0s. I'll open a separate evalml ticket for handling that, though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
makes sense!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We already don't allow using interpolation with categorical or boolean types. From the docstring:
categorical_impute_strategy (string): Impute strategy to use for string, object, boolean, categorical dtypes.
Valid values include "backwards_fill" and "forwards_fill". Defaults to "forwards_fill".
numeric_impute_strategy (string): Impute strategy to use for numeric columns. Valid values include
"backwards_fill", "forwards_fill", and "interpolate". Defaults to "interpolate".
It's enforced in a couple different places, between checking the arguments and how we select splitting between numeric and categorical!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I only ran into it with the target impute strategy, which I think doesn't have the same safeguards.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, very true - good point!
3f84baa
to
368b5a3
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM - great work!
# since the category dtype also has incompatibilities with linear interpolate, which is expected | ||
# --> i think this is essentially what happens now but won't that make floating point values that cant be turned back into bools? | ||
if isinstance(y.ww.logical_type, BooleanNullable): | ||
y = ww.init_series(y, Double) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
makes sense!
c159a46
to
1ceef7f
Compare
…pe incompatibiliy
1ceef7f
to
36723c8
Compare
closes #3991
Adds accurate
_integer_nullable_incompatibilities
and_boolean_nullable_incompatibilities
properties to the components that are known to have nullable type incompatibilities. Tests that those incompatibilities cause errors that are fixed by the handling. Also tests that components without incompatibilities can use the nullable types.Note - does not actually start calling
_handle_nullable_types
inside fit/predict/transform yet, as that would start using it in automl search. So we do not yet expect to see any impact to search performance or runtimes.