Skip to content
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

chore: improving sklearn compatibility via parametrize_with_checks #660

Merged

Conversation

FBruzzesi
Copy link
Collaborator

@FBruzzesi FBruzzesi commented May 9, 2024

Description

So the story goes as the following:

  • The CI/CD fails for scikit-learn==1.5rc1 because of a change in the check_estimator internals
  • In the scikit-learn issue I got a better picture of how to run test for compatible components
  • In particular, in rolling your own estimator suggests to use parametrize_with_checks, and of course I thought "that is a great idea to avoid dealing manually with each test"
  • Say no more, I enter a rabbit hole to refactor all our tests - which would be fine
  • Except that these tests failures helped me figure out a few missing parts in the codebase
  • Therefore I started to address those missing bits, and that's how you end up with 60+ 70 files changes

Current status of tests

  • In estimators:
    • DemographicParityClassifier and EqualOpportunityClassifier: n_features_in_ is missing (added those test to the skipping set)
    • QuantileRegression(..., solver='L-BFGS-B') fails when some sample_weights are zero.
  • In meta:
    • OrdinalClassifier fails tests because we check that y has at least 3 classes, and I could not find a way to flag that to scikit-learn tests (skipping all tests)
    • Thresholder needs some more love added love, now it passes tests.

@FBruzzesi FBruzzesi changed the title Tests/sklearn parametrize with checks tests: sklearn parametrize_with_checks May 9, 2024
pyproject.toml Outdated
Comment on lines 48 to 50
cvxpy = ["cmake", "osqp", "cvxpy>=1.1.8"]
formulaic = ["formulaic>=0.6.0"]
umap = ["umap-learn>=0.4.6", "numpy<2.0"]
umap = ["umap-learn>=0.4.6"]
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

That's the good news! They are both working with numpy 2.0rc


def transform(self, X):
Copy link
Collaborator Author

@FBruzzesi FBruzzesi May 9, 2024

Choose a reason for hiding this comment

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

This method was throwing off the checks as they dynamically look for method names

@@ -124,3 +124,6 @@ def allowed_strategies(self):
DeprecationWarning,
)
return self._ALLOWED_STRATEGIES

def _more_tags(self):
return {"poor_score": True, "non_deterministic": True}
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I love the poor_score tag 😂😂

Comment on lines 505 to 507
self.n_features_in_ = (
X.shape[1] - self.fit_intercept
) # + (1 - int(self.train_sensitive_cols)) * len(self.sensitive_col_idx_)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is not necessarily true

@@ -80,20 +78,3 @@ def test_fit_intercept_and_copy(coefs, intercept):
def test_lad(test_fn):
regr = LADRegression()
test_fn(LADRegression.__name__, regr)


@pytest.mark.parametrize("positive", [True, False])
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It's already tested in QuantileRegression, no need to duplicate them

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Edit: Actually the entire test can be considered a duplicate

@@ -17,8 +18,9 @@

def _create_dataset(coefs, intercept, noise=0.0):
np.random.seed(0)
X = np.random.randn(10000, coefs.shape[0])
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

going from 10_000 to 1000 was a big achievement to reduce runtime of the solvers

Copy link
Owner

@koaning koaning left a comment

Choose a reason for hiding this comment

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

All the changes look sensible sofar, but I am wondering if we're perhaps a bit early when it comes to implementing all of this ... the more tags stuff feels a bit experimental. If you don't mind the experimental nature of it then that is fine, but it also feels fine to ignore for now.

sklego/meta/_grouped_utils.py Outdated Show resolved Hide resolved
sklego/meta/ordinal_classification.py Show resolved Hide resolved
sklego/naive_bayes.py Show resolved Hide resolved
@FBruzzesi FBruzzesi marked this pull request as ready for review May 10, 2024 08:41
@FBruzzesi FBruzzesi changed the title tests: sklearn parametrize_with_checks chore: improving sklearn compatibility via parametrize_with_checks May 10, 2024
Comment on lines +1134 to +1143
return np.average(
np.where(X @ params < y, self.quantile, 1 - self.quantile) * np.abs(y - X @ params),
weights=sample_weight,
) + self._regularized_loss(params)

def grad_quantile_loss(params):
return (
-(sample_weight * np.where(X @ params < y, self.quantile, 1 - self.quantile) * np.sign(y - X @ params))
@ X
/ X.shape[0]
/ sample_weight.sum()
Copy link
Collaborator Author

@FBruzzesi FBruzzesi May 10, 2024

Choose a reason for hiding this comment

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

Hey @Garve, sorry to drag you into this mess.

No need for you to read all the changes. TL;DR is that in this PR I added tests using scikit-learn parametrize_with_checks to check for (better) compatibility.

As some of the tests were failing I took a closer look and adjusted the formulas to take into account sample_weight. However we still have tests failing for just one of the solvers. I wonder if you have any clue or insight on why this is the case.

Copy link
Owner

Choose a reason for hiding this comment

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

If it's really just one solver I would just not sweat it. We should document it for sure but we can always patch that later.

Copy link
Collaborator Author

@FBruzzesi FBruzzesi May 15, 2024

Choose a reason for hiding this comment

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

Apparently, for ubuntu and windows 'TNC' as well is failing, but only for quantile=0.3 and fit_intercept=False, not in the other cases (runs).
I am skipping that as well and improving the warning in the class description

Comment on lines -165 to +172
return self.gmm_.score_samples(X) * -1
return self.gmm_.score_samples(X)

def decision_function(self, X):
# We subtract self.offset_ to make 0 be the threshold value for being an outlier:
return self.score_samples(X) + self.likelihood_threshold_
return self.score_samples(X) - self.offset_
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Both this detector and gmm one required some (breaking changes) to comply to the tests and how scikit expects score_samples, decision_function and predict to interoperate

else:
try:
_ = self.estimator_.predict(X[:1])
check_is_fitted(self.estimator_)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

From the docs:

If None, estimator is considered fitted if there exist an attribute that ends with a underscore and does not start with double underscore.

I think it's a good proxy. The reason for this is that for non-array data, X is not subscriptable

@FBruzzesi FBruzzesi requested a review from koaning May 15, 2024 13:51
@FBruzzesi
Copy link
Collaborator Author

@koaning actually umap is failing on --pre with numpy==2.0rc2
proof

@koaning
Copy link
Owner

koaning commented May 15, 2024

Can't we add that numpy req to the umap requirements for the time being? Or does that break something else?

Pragmatically I would assume that umap is an edge case. Most folks probably won't rely on those specific components. The grouped stuff is way more pressing for the library.

@FBruzzesi
Copy link
Collaborator Author

@koaning I pinned back numpy<2.0 for the umap extra dependency.
Yesterday I learned that it's not supposed to be compatible yet anyway (comment)

Copy link
Owner

@koaning koaning left a comment

Choose a reason for hiding this comment

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

lgtm!

@FBruzzesi FBruzzesi merged commit 2ae8425 into koaning:main May 20, 2024
17 checks passed
@FBruzzesi FBruzzesi deleted the tests/sklearn-parametrize-with-checks branch May 20, 2024 15:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants