-
Notifications
You must be signed in to change notification settings - Fork 928
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 catboost integration tests #17931
base: branch-25.04
Are you sure you want to change the base?
Changes from all commits
647b9bc
2da2981
acab0c4
ebf3cd3
7828b38
14668ab
ee7890c
37caae6
7d3161b
b671426
1927f9e
0f6b775
164d81e
43cd327
fb5028d
5b3c87d
62660b3
14dae38
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,128 @@ | ||||||||||||||||||||||||||||||||||||||||
# Copyright (c) 2025, NVIDIA CORPORATION. | ||||||||||||||||||||||||||||||||||||||||
import numpy as np | ||||||||||||||||||||||||||||||||||||||||
import pandas as pd | ||||||||||||||||||||||||||||||||||||||||
import pytest | ||||||||||||||||||||||||||||||||||||||||
from catboost import CatBoostClassifier, CatBoostRegressor, Pool | ||||||||||||||||||||||||||||||||||||||||
from sklearn.datasets import make_classification, make_regression | ||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||
rng = np.random.default_rng(seed=42) | ||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||
def assert_catboost_equal(expect, got, rtol=1e-7, atol=0.0): | ||||||||||||||||||||||||||||||||||||||||
if isinstance(expect, (tuple, list)): | ||||||||||||||||||||||||||||||||||||||||
assert len(expect) == len(got) | ||||||||||||||||||||||||||||||||||||||||
for e, g in zip(expect, got): | ||||||||||||||||||||||||||||||||||||||||
assert_catboost_equal(e, g, rtol, atol) | ||||||||||||||||||||||||||||||||||||||||
elif isinstance(expect, np.ndarray): | ||||||||||||||||||||||||||||||||||||||||
np.testing.assert_allclose(expect, got, rtol=rtol, atol=atol) | ||||||||||||||||||||||||||||||||||||||||
elif isinstance(expect, pd.DataFrame): | ||||||||||||||||||||||||||||||||||||||||
pd.testing.assert_frame_equal(expect, got) | ||||||||||||||||||||||||||||||||||||||||
elif isinstance(expect, pd.Series): | ||||||||||||||||||||||||||||||||||||||||
pd.testing.assert_series_equal(expect, got) | ||||||||||||||||||||||||||||||||||||||||
else: | ||||||||||||||||||||||||||||||||||||||||
assert expect == got | ||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||
pytestmark = pytest.mark.assert_eq(fn=assert_catboost_equal) | ||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||
@pytest.fixture | ||||||||||||||||||||||||||||||||||||||||
def regression_data(): | ||||||||||||||||||||||||||||||||||||||||
X, y = make_regression(n_samples=100, n_features=10, random_state=42) | ||||||||||||||||||||||||||||||||||||||||
return pd.DataFrame(X), pd.Series(y) | ||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||
@pytest.fixture | ||||||||||||||||||||||||||||||||||||||||
def classification_data(): | ||||||||||||||||||||||||||||||||||||||||
X, y = make_classification( | ||||||||||||||||||||||||||||||||||||||||
n_samples=100, n_features=10, n_classes=2, random_state=42 | ||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
You may want to use slightly more data, here an in I've seen this before in LightGBM and XGBoost... someone will write a test that fits on a very small dataset and it'll look like nothing went wrong, only to later find that actually the dataset was so small that the model was just a collection of decision stumps (no splits), and so the test could never catch issues like "this encoding doesn't preserve NAs" or "these outputs are different because of numerical precision issues". |
||||||||||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
from sklearn.datasets import make_classification
X, y = make_classification(
n_samples=100, n_features=10, n_classes=2, random_state=42
)
X
For Encoding and decoding categorical features is critical to how CatBoost works (docs), and there are lots of things that have to go exactly right when providing I really think you should provide an input dataset that has some categorical features, ideally in 2 forms:
And ideally with varying cardinality. You could consider adapting this code used in And here are some docs on how to tell CatBoost which features are categorical: https://catboost.ai/docs/en/concepts/python-usages-examples#class-with-array-like-data-with-numerical,-categorical-and-embedding-features |
||||||||||||||||||||||||||||||||||||||||
return pd.DataFrame(X), pd.Series(y) | ||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||
def test_catboost_regressor_with_dataframe(regression_data): | ||||||||||||||||||||||||||||||||||||||||
X, y = regression_data | ||||||||||||||||||||||||||||||||||||||||
model = CatBoostRegressor(iterations=10, verbose=0) | ||||||||||||||||||||||||||||||||||||||||
model.fit(X, y) | ||||||||||||||||||||||||||||||||||||||||
predictions = model.predict(X) | ||||||||||||||||||||||||||||||||||||||||
return predictions | ||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||
def test_catboost_regressor_with_numpy(regression_data): | ||||||||||||||||||||||||||||||||||||||||
X, y = regression_data | ||||||||||||||||||||||||||||||||||||||||
model = CatBoostRegressor(iterations=10, verbose=0) | ||||||||||||||||||||||||||||||||||||||||
model.fit(X.values, y.values) | ||||||||||||||||||||||||||||||||||||||||
predictions = model.predict(X.values) | ||||||||||||||||||||||||||||||||||||||||
return predictions | ||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry in advance, I'm not that familiar with these tests but... I'm surprised to see pytestmark = pytest.mark.assert_eq(fn=assert_catboost_equal) Did you mean for there to be some kind of testing assertion here? Or does that custom marker somehow end up invoking that function and comparing the output of the test case with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The assertion function is used to check that results from "cudf.pandas on" and "cudf.pandas off" are equal. The logic to handle that is in the conftest file. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Thanks, I wasn't sure which cudf/python/cudf/cudf_pandas_tests/third_party_integration_tests/tests/conftest.py Lines 130 to 148 in 300d816
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yup! |
||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||
def test_catboost_classifier_with_dataframe(classification_data): | ||||||||||||||||||||||||||||||||||||||||
X, y = classification_data | ||||||||||||||||||||||||||||||||||||||||
model = CatBoostClassifier(iterations=10, verbose=0) | ||||||||||||||||||||||||||||||||||||||||
model.fit(X, y) | ||||||||||||||||||||||||||||||||||||||||
predictions = model.predict(X) | ||||||||||||||||||||||||||||||||||||||||
return predictions | ||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||
def test_catboost_classifier_with_numpy(classification_data): | ||||||||||||||||||||||||||||||||||||||||
X, y = classification_data | ||||||||||||||||||||||||||||||||||||||||
model = CatBoostClassifier(iterations=10, verbose=0) | ||||||||||||||||||||||||||||||||||||||||
model.fit(X.values, y.values) | ||||||||||||||||||||||||||||||||||||||||
predictions = model.predict(X.values) | ||||||||||||||||||||||||||||||||||||||||
return predictions | ||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||
def test_catboost_with_pool_and_dataframe(regression_data): | ||||||||||||||||||||||||||||||||||||||||
X, y = regression_data | ||||||||||||||||||||||||||||||||||||||||
train_pool = Pool(X, y) | ||||||||||||||||||||||||||||||||||||||||
model = CatBoostRegressor(iterations=10, verbose=0) | ||||||||||||||||||||||||||||||||||||||||
model.fit(train_pool) | ||||||||||||||||||||||||||||||||||||||||
predictions = model.predict(X) | ||||||||||||||||||||||||||||||||||||||||
return predictions | ||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||
def test_catboost_with_pool_and_numpy(regression_data): | ||||||||||||||||||||||||||||||||||||||||
X, y = regression_data | ||||||||||||||||||||||||||||||||||||||||
train_pool = Pool(X.values, y.values) | ||||||||||||||||||||||||||||||||||||||||
model = CatBoostRegressor(iterations=10, verbose=0) | ||||||||||||||||||||||||||||||||||||||||
model.fit(train_pool) | ||||||||||||||||||||||||||||||||||||||||
predictions = model.predict(X.values) | ||||||||||||||||||||||||||||||||||||||||
return predictions | ||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||
def test_catboost_with_categorical_features(): | ||||||||||||||||||||||||||||||||||||||||
data = { | ||||||||||||||||||||||||||||||||||||||||
"numerical_feature": rng.standard_normal(100), | ||||||||||||||||||||||||||||||||||||||||
"categorical_feature": rng.choice(["A", "B", "C"], size=100), | ||||||||||||||||||||||||||||||||||||||||
"target": rng.integers(0, 2, size=100), | ||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||
df = pd.DataFrame(data) | ||||||||||||||||||||||||||||||||||||||||
X = df[["numerical_feature", "categorical_feature"]] | ||||||||||||||||||||||||||||||||||||||||
y = df["target"] | ||||||||||||||||||||||||||||||||||||||||
cat_features = ["categorical_feature"] | ||||||||||||||||||||||||||||||||||||||||
model = CatBoostClassifier( | ||||||||||||||||||||||||||||||||||||||||
iterations=10, verbose=0, cat_features=cat_features | ||||||||||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||||||||||
model.fit(X, y) | ||||||||||||||||||||||||||||||||||||||||
predictions = model.predict(X) | ||||||||||||||||||||||||||||||||||||||||
return predictions | ||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||
@pytest.mark.parametrize( | ||||||||||||||||||||||||||||||||||||||||
"X, y", | ||||||||||||||||||||||||||||||||||||||||
[ | ||||||||||||||||||||||||||||||||||||||||
( | ||||||||||||||||||||||||||||||||||||||||
pd.DataFrame(rng.standard_normal((100, 5))), | ||||||||||||||||||||||||||||||||||||||||
pd.Series(rng.standard_normal(100)), | ||||||||||||||||||||||||||||||||||||||||
), | ||||||||||||||||||||||||||||||||||||||||
(rng.standard_normal((100, 5)), rng.standard_normal(100)), | ||||||||||||||||||||||||||||||||||||||||
], | ||||||||||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||||||||||
def test_catboost_train_test_split(X, y): | ||||||||||||||||||||||||||||||||||||||||
from sklearn.model_selection import train_test_split | ||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42) | ||||||||||||||||||||||||||||||||||||||||
model = CatBoostRegressor(iterations=10, verbose=0) | ||||||||||||||||||||||||||||||||||||||||
model.fit(X_train, y_train) | ||||||||||||||||||||||||||||||||||||||||
predictions = model.predict(X_test) | ||||||||||||||||||||||||||||||||||||||||
return len(X_train), len(X_test), len(y_train), len(y_test), predictions |
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.
See this paragraph from the numpy 2 release