Files
lightgbm-org--lightgbm/tests/python_package_test/test_sklearn.py
T
2026-07-13 13:27:18 +08:00

2368 lines
100 KiB
Python

# coding: utf-8
import inspect
import itertools
import math
import re
import sys
import warnings
from functools import partial
from pathlib import Path
import joblib
import numpy as np
import pytest
import scipy.sparse
from scipy.stats import spearmanr
from sklearn.base import clone
from sklearn.calibration import CalibratedClassifierCV
from sklearn.datasets import load_svmlight_file, make_blobs, make_multilabel_classification
from sklearn.ensemble import StackingClassifier, StackingRegressor
from sklearn.metrics import accuracy_score, log_loss, mean_squared_error, r2_score
from sklearn.model_selection import GridSearchCV, RandomizedSearchCV, train_test_split
from sklearn.multioutput import ClassifierChain, MultiOutputClassifier, MultiOutputRegressor, RegressorChain
from sklearn.utils.estimator_checks import parametrize_with_checks as sklearn_parametrize_with_checks
from sklearn.utils.validation import check_is_fitted
import lightgbm as lgb
from lightgbm.basic import LGBMDeprecationWarning
from lightgbm.compat import (
PANDAS_INSTALLED,
_sklearn_version,
pd_DataFrame,
pd_Series,
)
from .utils import (
BuildInfo,
assert_silent,
load_breast_cancer,
load_digits,
load_iris,
load_linnerud,
logistic_sigmoid,
make_ranking,
make_synthetic_regression,
np_assert_array_equal,
sklearn_multiclass_custom_objective,
softmax,
)
SKLEARN_MAJOR, SKLEARN_MINOR, *_ = _sklearn_version.split(".")
SKLEARN_VERSION_GTE_1_6 = (int(SKLEARN_MAJOR), int(SKLEARN_MINOR)) >= (1, 6)
SKLEARN_VERSION_GTE_1_7 = (int(SKLEARN_MAJOR), int(SKLEARN_MINOR)) >= (1, 7)
decreasing_generator = itertools.count(0, -1)
estimator_classes = (lgb.LGBMModel, lgb.LGBMClassifier, lgb.LGBMRegressor, lgb.LGBMRanker)
task_to_model_factory = {
"ranking": lgb.LGBMRanker,
"binary-classification": lgb.LGBMClassifier,
"multiclass-classification": lgb.LGBMClassifier,
"regression": lgb.LGBMRegressor,
}
all_tasks = tuple(task_to_model_factory.keys())
all_x_types = ("list2d", "numpy", "pd_DataFrame", "pa_Table", "pl_DataFrame", "scipy_csc", "scipy_csr")
all_y_types = ("list1d", "numpy", "pd_Series", "pd_DataFrame", "pa_ChunkedArray", "pl_Series")
all_group_types = ("list1d_float", "list1d_int", "numpy", "pd_Series", "pa_ChunkedArray", "pl_Series")
def _create_data(task, n_samples=100, n_features=4):
if task == "ranking":
X, y, g = make_ranking(n_features=4, n_samples=n_samples)
g = np.bincount(g)
elif task.endswith("classification"):
if task == "binary-classification":
centers = 2
elif task == "multiclass-classification":
centers = 3
else:
raise ValueError(f"Unknown classification task '{task}'")
X, y = make_blobs(n_samples=n_samples, n_features=n_features, centers=centers, random_state=42)
g = None
elif task == "regression":
X, y = make_synthetic_regression(n_samples=n_samples, n_features=n_features)
g = None
return X, y, g
class UnpicklableCallback:
def __reduce__(self):
raise Exception("This class in not picklable")
def __call__(self, env):
env.model.attr_set_inside_callback = env.iteration * 10
class ExtendedLGBMClassifier(lgb.LGBMClassifier):
"""Class for testing that inheriting from LGBMClassifier works"""
def __init__(self, *, some_other_param: str = "lgbm-classifier", **kwargs):
self.some_other_param = some_other_param
super().__init__(**kwargs)
class ExtendedLGBMRanker(lgb.LGBMRanker):
"""Class for testing that inheriting from LGBMRanker works"""
def __init__(self, *, some_other_param: str = "lgbm-ranker", **kwargs):
self.some_other_param = some_other_param
super().__init__(**kwargs)
class ExtendedLGBMRegressor(lgb.LGBMRegressor):
"""Class for testing that inheriting from LGBMRegressor works"""
def __init__(self, *, some_other_param: str = "lgbm-regressor", **kwargs):
self.some_other_param = some_other_param
super().__init__(**kwargs)
def custom_asymmetric_obj(y_true, y_pred):
residual = (y_true - y_pred).astype(np.float64)
grad = np.where(residual < 0, -2 * 10.0 * residual, -2 * residual)
hess = np.where(residual < 0, 2 * 10.0, 2.0)
return grad, hess
def objective_ls(y_true, y_pred):
grad = y_pred - y_true
hess = np.ones(len(y_true))
return grad, hess
def logregobj(y_true, y_pred):
y_pred = 1.0 / (1.0 + np.exp(-y_pred))
grad = y_pred - y_true
hess = y_pred * (1.0 - y_pred)
return grad, hess
def custom_dummy_obj(y_true, y_pred):
return np.ones(y_true.shape), np.ones(y_true.shape)
def constant_metric(y_true, y_pred):
return "error", 0, False
def decreasing_metric(y_true, y_pred):
return ("decreasing_metric", next(decreasing_generator), False)
def mse(y_true, y_pred):
return "custom MSE", mean_squared_error(y_true, y_pred), False
def binary_error(y_true, y_pred):
return np.mean((y_pred > 0.5) != y_true)
def multi_error(y_true, y_pred):
return np.mean(y_true != y_pred)
def multi_logloss(y_true, y_pred):
return np.mean([-math.log(y_pred[i][y]) for i, y in enumerate(y_true)])
def test_binary():
X, y = load_breast_cancer(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, random_state=42)
gbm = lgb.LGBMClassifier(n_estimators=50, verbose=-1)
gbm.fit(X_train, y_train, eval_set=[(X_test, y_test)], callbacks=[lgb.early_stopping(5)])
ret = log_loss(y_test, gbm.predict_proba(X_test))
assert ret < 0.12
assert gbm.evals_result_["valid_0"]["binary_logloss"][gbm.best_iteration_ - 1] == pytest.approx(ret)
def test_regression():
X, y = make_synthetic_regression()
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, random_state=42)
gbm = lgb.LGBMRegressor(n_estimators=50, verbose=-1)
gbm.fit(X_train, y_train, eval_set=[(X_test, y_test)], callbacks=[lgb.early_stopping(5)])
ret = mean_squared_error(y_test, gbm.predict(X_test))
assert ret < 174
assert gbm.evals_result_["valid_0"]["l2"][gbm.best_iteration_ - 1] == pytest.approx(ret)
@pytest.mark.skipif(BuildInfo.has_cuda, reason="Skip due to differences in implementation details of CUDA version")
def test_multiclass():
X, y = load_digits(n_class=10, return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, random_state=42)
gbm = lgb.LGBMClassifier(n_estimators=50, verbose=-1)
gbm.fit(X_train, y_train, eval_set=[(X_test, y_test)], callbacks=[lgb.early_stopping(5)])
ret = multi_error(y_test, gbm.predict(X_test))
assert ret < 0.05
ret = multi_logloss(y_test, gbm.predict_proba(X_test))
assert ret < 0.16
assert gbm.evals_result_["valid_0"]["multi_logloss"][gbm.best_iteration_ - 1] == pytest.approx(ret)
@pytest.mark.skipif(BuildInfo.has_cuda, reason="Skip due to differences in implementation details of CUDA version")
def test_lambdarank():
rank_example_dir = Path(__file__).absolute().parents[2] / "examples" / "lambdarank"
X_train, y_train = load_svmlight_file(str(rank_example_dir / "rank.train"))
X_test, y_test = load_svmlight_file(str(rank_example_dir / "rank.test"))
q_train = np.loadtxt(str(rank_example_dir / "rank.train.query"))
q_test = np.loadtxt(str(rank_example_dir / "rank.test.query"))
gbm = lgb.LGBMRanker(n_estimators=50)
gbm.fit(
X_train,
y_train,
group=q_train,
eval_set=[(X_test, y_test)],
eval_group=[q_test],
eval_at=[1, 3],
callbacks=[lgb.early_stopping(10), lgb.reset_parameter(learning_rate=lambda x: max(0.01, 0.1 - 0.01 * x))],
)
assert gbm.best_iteration_ <= 24
assert gbm.best_score_["valid_0"]["ndcg@1"] > 0.5674
assert gbm.best_score_["valid_0"]["ndcg@3"] > 0.578
def test_xendcg():
xendcg_example_dir = Path(__file__).absolute().parents[2] / "examples" / "xendcg"
X_train, y_train = load_svmlight_file(str(xendcg_example_dir / "rank.train"))
X_test, y_test = load_svmlight_file(str(xendcg_example_dir / "rank.test"))
q_train = np.loadtxt(str(xendcg_example_dir / "rank.train.query"))
q_test = np.loadtxt(str(xendcg_example_dir / "rank.test.query"))
gbm = lgb.LGBMRanker(n_estimators=50, objective="rank_xendcg", random_state=5, n_jobs=1)
gbm.fit(
X_train,
y_train,
group=q_train,
eval_set=[(X_test, y_test)],
eval_group=[q_test],
eval_at=[1, 3],
eval_metric="ndcg",
callbacks=[lgb.early_stopping(10), lgb.reset_parameter(learning_rate=lambda x: max(0.01, 0.1 - 0.01 * x))],
)
assert gbm.best_iteration_ <= 24
assert gbm.best_score_["valid_0"]["ndcg@1"] > 0.6211
assert gbm.best_score_["valid_0"]["ndcg@3"] > 0.6253
def test_eval_at_aliases():
rank_example_dir = Path(__file__).absolute().parents[2] / "examples" / "lambdarank"
X_train, y_train = load_svmlight_file(str(rank_example_dir / "rank.train"))
X_test, y_test = load_svmlight_file(str(rank_example_dir / "rank.test"))
q_train = np.loadtxt(str(rank_example_dir / "rank.train.query"))
q_test = np.loadtxt(str(rank_example_dir / "rank.test.query"))
for alias in lgb.basic._ConfigAliases.get("eval_at"):
gbm = lgb.LGBMRanker(n_estimators=5, **{alias: [1, 2, 3, 9]})
with pytest.warns(UserWarning, match=f"Found '{alias}' in params. Will use it instead of 'eval_at' argument"):
gbm.fit(X_train, y_train, group=q_train, eval_set=[(X_test, y_test)], eval_group=[q_test])
assert list(gbm.evals_result_["valid_0"].keys()) == ["ndcg@1", "ndcg@2", "ndcg@3", "ndcg@9"]
@pytest.mark.parametrize("custom_objective", [True, False])
def test_objective_aliases(custom_objective):
X, y = make_synthetic_regression()
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, random_state=42)
if custom_objective:
obj = custom_dummy_obj
metric_name = "l2" # default one
else:
obj = "mape"
metric_name = "mape"
evals = []
for alias in lgb.basic._ConfigAliases.get("objective"):
gbm = lgb.LGBMRegressor(n_estimators=5, **{alias: obj})
if alias != "objective":
with pytest.warns(
UserWarning, match=f"Found '{alias}' in params. Will use it instead of 'objective' argument"
):
gbm.fit(X_train, y_train, eval_set=[(X_test, y_test)])
else:
gbm.fit(X_train, y_train, eval_set=[(X_test, y_test)])
assert list(gbm.evals_result_["valid_0"].keys()) == [metric_name]
evals.append(gbm.evals_result_["valid_0"][metric_name])
evals_t = np.array(evals).T
for i in range(evals_t.shape[0]):
np.testing.assert_allclose(evals_t[i], evals_t[i][0])
# check that really dummy objective was used and estimator didn't learn anything
if custom_objective:
np.testing.assert_allclose(evals_t, evals_t[0][0])
def test_regression_with_custom_objective():
X, y = make_synthetic_regression()
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, random_state=42)
gbm = lgb.LGBMRegressor(n_estimators=50, verbose=-1, objective=objective_ls)
gbm.fit(X_train, y_train, eval_set=[(X_test, y_test)], callbacks=[lgb.early_stopping(5)])
ret = mean_squared_error(y_test, gbm.predict(X_test))
assert ret < 174
assert gbm.evals_result_["valid_0"]["l2"][gbm.best_iteration_ - 1] == pytest.approx(ret)
def test_binary_classification_with_custom_objective():
X, y = load_digits(n_class=2, return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, random_state=42)
gbm = lgb.LGBMClassifier(n_estimators=50, verbose=-1, objective=logregobj)
gbm.fit(X_train, y_train, eval_set=[(X_test, y_test)], callbacks=[lgb.early_stopping(5)])
# prediction result is actually not transformed (is raw) due to custom objective
y_pred_raw = gbm.predict_proba(X_test)
assert not np.all(y_pred_raw >= 0)
y_pred = 1.0 / (1.0 + np.exp(-y_pred_raw))
ret = binary_error(y_test, y_pred)
assert ret < 0.05
def test_dart():
X, y = make_synthetic_regression()
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, random_state=42)
gbm = lgb.LGBMRegressor(boosting_type="dart", n_estimators=50)
gbm.fit(X_train, y_train)
score = gbm.score(X_test, y_test)
assert 0.8 <= score <= 1.0
def test_stacking_classifier():
X, y = load_iris(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)
classifiers = [("gbm1", lgb.LGBMClassifier(n_estimators=3)), ("gbm2", lgb.LGBMClassifier(n_estimators=3))]
clf = StackingClassifier(
estimators=classifiers, final_estimator=lgb.LGBMClassifier(n_estimators=3), passthrough=True
)
clf.fit(X_train, y_train)
score = clf.score(X_test, y_test)
assert score >= 0.8
assert score <= 1.0
assert clf.n_features_in_ == 4 # number of input features
assert len(clf.named_estimators_["gbm1"].feature_importances_) == 4
assert clf.named_estimators_["gbm1"].n_features_in_ == clf.named_estimators_["gbm2"].n_features_in_
assert clf.final_estimator_.n_features_in_ == 10 # number of concatenated features
assert len(clf.final_estimator_.feature_importances_) == 10
assert all(clf.named_estimators_["gbm1"].classes_ == clf.named_estimators_["gbm2"].classes_)
assert all(clf.classes_ == clf.named_estimators_["gbm1"].classes_)
def test_stacking_regressor():
X, y = make_synthetic_regression(n_samples=200)
n_features = X.shape[1]
n_input_models = 2
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)
regressors = [("gbm1", lgb.LGBMRegressor(n_estimators=3)), ("gbm2", lgb.LGBMRegressor(n_estimators=3))]
reg = StackingRegressor(estimators=regressors, final_estimator=lgb.LGBMRegressor(n_estimators=3), passthrough=True)
reg.fit(X_train, y_train)
score = reg.score(X_test, y_test)
assert score >= 0.2
assert score <= 1.0
assert reg.n_features_in_ == n_features # number of input features
assert len(reg.named_estimators_["gbm1"].feature_importances_) == n_features
assert reg.named_estimators_["gbm1"].n_features_in_ == reg.named_estimators_["gbm2"].n_features_in_
assert reg.final_estimator_.n_features_in_ == n_features + n_input_models # number of concatenated features
assert len(reg.final_estimator_.feature_importances_) == n_features + n_input_models
def test_grid_search():
X, y = load_iris(return_X_y=True)
y = y.astype(str) # utilize label encoder at it's max power
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, random_state=42)
X_train, X_val, y_train, y_val = train_test_split(X_train, y_train, test_size=0.1, random_state=42)
params = {"subsample": 0.8, "subsample_freq": 1}
grid_params = {"boosting_type": ["rf", "gbdt"], "n_estimators": [4, 6], "reg_alpha": [0.01, 0.005]}
evals_result = {}
fit_params = {
"eval_set": [(X_val, y_val)],
"eval_metric": constant_metric,
"callbacks": [lgb.early_stopping(2), lgb.record_evaluation(evals_result)],
}
grid = GridSearchCV(estimator=lgb.LGBMClassifier(**params), param_grid=grid_params, cv=2)
grid.fit(X_train, y_train, **fit_params)
score = grid.score(X_test, y_test) # utilizes GridSearchCV default refit=True
assert grid.best_params_["boosting_type"] in ["rf", "gbdt"]
assert grid.best_params_["n_estimators"] in [4, 6]
assert grid.best_params_["reg_alpha"] in [0.01, 0.005]
assert grid.best_score_ <= 1.0
assert grid.best_estimator_.best_iteration_ == 1
assert grid.best_estimator_.best_score_["valid_0"]["multi_logloss"] < 0.25
assert grid.best_estimator_.best_score_["valid_0"]["error"] == 0
assert score >= 0.2
assert score <= 1.0
assert evals_result == grid.best_estimator_.evals_result_
def test_random_search(rng):
X, y = load_iris(return_X_y=True)
y = y.astype(str) # utilize label encoder at it's max power
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, random_state=42)
X_train, X_val, y_train, y_val = train_test_split(X_train, y_train, test_size=0.1, random_state=42)
n_iter = 3 # Number of samples
params = {"subsample": 0.8, "subsample_freq": 1}
param_dist = {
"boosting_type": ["rf", "gbdt"],
"n_estimators": rng.integers(low=3, high=10, size=(n_iter,)).tolist(),
"reg_alpha": rng.uniform(low=0.01, high=0.06, size=(n_iter,)).tolist(),
}
fit_params = {"eval_set": [(X_val, y_val)], "eval_metric": constant_metric, "callbacks": [lgb.early_stopping(2)]}
rand = RandomizedSearchCV(
estimator=lgb.LGBMClassifier(**params), param_distributions=param_dist, cv=2, n_iter=n_iter, random_state=42
)
rand.fit(X_train, y_train, **fit_params)
score = rand.score(X_test, y_test) # utilizes RandomizedSearchCV default refit=True
assert rand.best_params_["boosting_type"] in ["rf", "gbdt"]
assert rand.best_params_["n_estimators"] in list(range(3, 10))
assert rand.best_params_["reg_alpha"] >= 0.01 # Left-closed boundary point
assert rand.best_params_["reg_alpha"] <= 0.06 # Right-closed boundary point
assert rand.best_score_ <= 1.0
assert rand.best_estimator_.best_score_["valid_0"]["multi_logloss"] < 0.25
assert rand.best_estimator_.best_score_["valid_0"]["error"] == 0
assert score >= 0.2
assert score <= 1.0
def test_multioutput_classifier():
n_outputs = 3
X, y = make_multilabel_classification(n_samples=100, n_features=20, n_classes=n_outputs, random_state=0)
y = y.astype(str) # utilize label encoder at it's max power
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, random_state=42)
clf = MultiOutputClassifier(estimator=lgb.LGBMClassifier(n_estimators=10))
clf.fit(X_train, y_train)
score = clf.score(X_test, y_test)
assert score >= 0.2
assert score <= 1.0
np_assert_array_equal(np.tile(np.unique(y_train), n_outputs), np.concatenate(clf.classes_), strict=True)
for classifier in clf.estimators_:
assert isinstance(classifier, lgb.LGBMClassifier)
assert isinstance(classifier.booster_, lgb.Booster)
def test_multioutput_regressor():
bunch = load_linnerud(as_frame=True) # returns a Bunch instance
X, y = bunch["data"], bunch["target"]
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, random_state=42)
reg = MultiOutputRegressor(estimator=lgb.LGBMRegressor(n_estimators=10))
reg.fit(X_train, y_train)
y_pred = reg.predict(X_test)
_, score, _ = mse(y_test, y_pred)
assert score >= 0.2
assert score <= 120.0
for regressor in reg.estimators_:
assert isinstance(regressor, lgb.LGBMRegressor)
assert isinstance(regressor.booster_, lgb.Booster)
def test_classifier_chain():
n_outputs = 3
X, y = make_multilabel_classification(n_samples=100, n_features=20, n_classes=n_outputs, random_state=0)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, random_state=42)
order = [2, 0, 1]
# 'base_estimator' parameter was deprecated in scikit-learn 1.7 and removed in 1.9
#
# * https://github.com/scikit-learn/scikit-learn/pull/30152
# * https://github.com/scikit-learn/scikit-learn/pull/33750
#
if SKLEARN_VERSION_GTE_1_7:
clf = ClassifierChain(estimator=lgb.LGBMClassifier(n_estimators=10), order=order, random_state=42)
else:
clf = ClassifierChain(base_estimator=lgb.LGBMClassifier(n_estimators=10), order=order, random_state=42)
clf.fit(X_train, y_train)
score = clf.score(X_test, y_test)
assert score >= 0.2
assert score <= 1.0
np_assert_array_equal(np.tile(np.unique(y_train), n_outputs), np.concatenate(clf.classes_), strict=True)
assert order == clf.order_
for classifier in clf.estimators_:
assert isinstance(classifier, lgb.LGBMClassifier)
assert isinstance(classifier.booster_, lgb.Booster)
def test_regressor_chain():
bunch = load_linnerud(as_frame=True) # returns a Bunch instance
X, y = bunch["data"], bunch["target"]
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, random_state=42)
order = [2, 0, 1]
# 'base_estimator' parameter was deprecated in scikit-learn 1.7 and removed in 1.9
#
# * https://github.com/scikit-learn/scikit-learn/pull/30152
# * https://github.com/scikit-learn/scikit-learn/pull/33750
#
if SKLEARN_VERSION_GTE_1_7:
reg = RegressorChain(estimator=lgb.LGBMRegressor(n_estimators=10), order=order, random_state=42)
else:
reg = RegressorChain(base_estimator=lgb.LGBMRegressor(n_estimators=10), order=order, random_state=42)
reg.fit(X_train, y_train)
y_pred = reg.predict(X_test)
_, score, _ = mse(y_test, y_pred)
assert score >= 0.2
assert score <= 120.0
assert order == reg.order_
for regressor in reg.estimators_:
assert isinstance(regressor, lgb.LGBMRegressor)
assert isinstance(regressor.booster_, lgb.Booster)
def test_clone_and_property():
X, y = make_synthetic_regression()
gbm = lgb.LGBMRegressor(n_estimators=10, verbose=-1)
gbm.fit(X, y)
gbm_clone = clone(gbm)
# original estimator is unaffected
assert gbm.n_estimators == 10
assert gbm.verbose == -1
assert isinstance(gbm.booster_, lgb.Booster)
assert isinstance(gbm.feature_importances_, np.ndarray)
# new estimator is unfitted, but has the same parameters
assert gbm_clone.__sklearn_is_fitted__() is False
assert gbm_clone.n_estimators == 10
assert gbm_clone.verbose == -1
assert gbm_clone.get_params() == gbm.get_params()
X, y = load_digits(n_class=2, return_X_y=True)
clf = lgb.LGBMClassifier(n_estimators=10, verbose=-1)
clf.fit(X, y)
assert sorted(clf.classes_) == [0, 1]
assert clf.n_classes_ == 2
assert isinstance(clf.booster_, lgb.Booster)
assert isinstance(clf.feature_importances_, np.ndarray)
@pytest.mark.parametrize("estimator", (lgb.LGBMClassifier, lgb.LGBMRegressor, lgb.LGBMRanker)) # noqa: PT007
def test_estimators_all_have_the_same_kwargs_and_defaults(estimator):
base_spec = inspect.getfullargspec(lgb.LGBMModel)
subclass_spec = inspect.getfullargspec(estimator)
# should not allow for any varargs
assert subclass_spec.varargs == base_spec.varargs
assert subclass_spec.varargs is None
# the only varkw should be **kwargs,
assert subclass_spec.varkw == base_spec.varkw
assert subclass_spec.varkw == "kwargs"
# default values for all constructor arguments should be identical
#
# NOTE: if LGBMClassifier / LGBMRanker / LGBMRegressor ever override
# any of LGBMModel's constructor arguments, this will need to be updated
assert subclass_spec.kwonlydefaults == base_spec.kwonlydefaults
# only positional argument should be 'self'
assert subclass_spec.args == base_spec.args
assert subclass_spec.args == ["self"]
assert subclass_spec.defaults is None
# get_params() should be identical
assert estimator().get_params() == lgb.LGBMModel().get_params()
def test_subclassing_get_params_works():
expected_params = {
"boosting_type": "gbdt",
"class_weight": None,
"colsample_bytree": 1.0,
"importance_type": "split",
"learning_rate": 0.1,
"max_depth": -1,
"min_child_samples": 20,
"min_child_weight": 0.001,
"min_split_gain": 0.0,
"n_estimators": 100,
"n_jobs": None,
"num_leaves": 31,
"objective": None,
"random_state": None,
"reg_alpha": 0.0,
"reg_lambda": 0.0,
"subsample": 1.0,
"subsample_for_bin": 200000,
"subsample_freq": 0,
}
# Overrides, used to test that passing through **kwargs works as expected.
#
# why these?
#
# - 'n_estimators' directly matches a keyword arg for the scikit-learn estimators
# - 'eta' is a parameter alias for 'learning_rate'
overrides = {"n_estimators": 13, "eta": 0.07}
# lightgbm-official classes
for est in [lgb.LGBMModel, lgb.LGBMClassifier, lgb.LGBMRanker, lgb.LGBMRegressor]:
assert est().get_params() == expected_params
assert est(**overrides).get_params() == {
**expected_params,
"eta": 0.07,
"n_estimators": 13,
"learning_rate": 0.1,
}
try:
import dask # noqa: F401,PLC0415
except lgb.dask._DaskImportErrorTypes:
pass
if "dask" in sys.modules:
for est in [lgb.DaskLGBMClassifier, lgb.DaskLGBMRanker, lgb.DaskLGBMRegressor]:
assert est().get_params() == {
**expected_params,
"client": None,
}
assert est(**overrides).get_params() == {
**expected_params,
"eta": 0.07,
"n_estimators": 13,
"learning_rate": 0.1,
"client": None,
}
# custom sub-classes
assert ExtendedLGBMClassifier().get_params() == {**expected_params, "some_other_param": "lgbm-classifier"}
assert ExtendedLGBMClassifier(**overrides).get_params() == {
**expected_params,
"eta": 0.07,
"n_estimators": 13,
"learning_rate": 0.1,
"some_other_param": "lgbm-classifier",
}
assert ExtendedLGBMRanker().get_params() == {
**expected_params,
"some_other_param": "lgbm-ranker",
}
assert ExtendedLGBMRanker(**overrides).get_params() == {
**expected_params,
"eta": 0.07,
"n_estimators": 13,
"learning_rate": 0.1,
"some_other_param": "lgbm-ranker",
}
assert ExtendedLGBMRegressor().get_params() == {
**expected_params,
"some_other_param": "lgbm-regressor",
}
assert ExtendedLGBMRegressor(**overrides).get_params() == {
**expected_params,
"eta": 0.07,
"n_estimators": 13,
"learning_rate": 0.1,
"some_other_param": "lgbm-regressor",
}
@pytest.mark.parametrize("task", all_tasks)
def test_subclassing_works(task):
# param values to make training deterministic and
# just train a small, cheap model
params = {
"deterministic": True,
"force_row_wise": True,
"n_jobs": 1,
"n_estimators": 5,
"num_leaves": 11,
"random_state": 708,
}
X, y, g = _create_data(task=task)
if task == "ranking":
est = lgb.LGBMRanker(**params).fit(X, y, group=g)
est_sub = ExtendedLGBMRanker(**params).fit(X, y, group=g)
elif task.endswith("classification"):
est = lgb.LGBMClassifier(**params).fit(X, y)
est_sub = ExtendedLGBMClassifier(**params).fit(X, y)
else:
est = lgb.LGBMRegressor(**params).fit(X, y)
est_sub = ExtendedLGBMRegressor(**params).fit(X, y)
np.testing.assert_allclose(est.predict(X), est_sub.predict(X))
@pytest.mark.parametrize(
"estimator_to_task",
[
(lgb.LGBMClassifier, "binary-classification"),
(ExtendedLGBMClassifier, "binary-classification"),
(lgb.LGBMRanker, "ranking"),
(ExtendedLGBMRanker, "ranking"),
(lgb.LGBMRegressor, "regression"),
(ExtendedLGBMRegressor, "regression"),
],
)
def test_parameter_aliases_are_handled_correctly(estimator_to_task):
estimator, task = estimator_to_task
# scikit-learn estimators should remember every parameter passed
# via keyword arguments in the estimator constructor, but then
# only pass the correct value down to LightGBM's C++ side
params = {
"eta": 0.08,
"num_iterations": 3,
"num_leaves": 5,
}
X, y, g = _create_data(task=task)
mod = estimator(**params)
if task == "ranking":
mod.fit(X, y, group=g)
else:
mod.fit(X, y)
# scikit-learn get_params()
p = mod.get_params()
assert p["eta"] == 0.08
assert p["learning_rate"] == 0.1
# lgb.Booster's 'params' attribute
p = mod.booster_.params
assert p["eta"] == 0.08
assert p["learning_rate"] == 0.1
# Config in the 'LightGBM::Booster' on the C++ side
p = mod.booster_._get_loaded_param()
assert p["learning_rate"] == 0.1
assert "eta" not in p
def test_joblib(tmp_path):
X, y = make_synthetic_regression()
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, random_state=42)
gbm = lgb.LGBMRegressor(n_estimators=10, objective=custom_asymmetric_obj, verbose=-1, importance_type="split")
gbm.fit(
X_train,
y_train,
eval_set=[(X_train, y_train), (X_test, y_test)],
eval_metric=mse,
callbacks=[lgb.early_stopping(5), lgb.reset_parameter(learning_rate=list(np.arange(1, 0, -0.1)))],
)
model_path_pkl = str(tmp_path / "lgb.pkl")
joblib.dump(gbm, model_path_pkl) # test model with custom functions
gbm_pickle = joblib.load(model_path_pkl)
assert isinstance(gbm_pickle.booster_, lgb.Booster)
assert gbm.get_params() == gbm_pickle.get_params()
np_assert_array_equal(gbm.feature_importances_, gbm_pickle.feature_importances_, strict=True)
assert gbm_pickle.learning_rate == pytest.approx(0.1)
assert callable(gbm_pickle.objective)
for eval_set in gbm.evals_result_:
for metric in gbm.evals_result_[eval_set]:
np.testing.assert_allclose(gbm.evals_result_[eval_set][metric], gbm_pickle.evals_result_[eval_set][metric])
pred_origin = gbm.predict(X_test)
pred_pickle = gbm_pickle.predict(X_test)
np.testing.assert_allclose(pred_origin, pred_pickle)
def test_non_serializable_objects_in_callbacks(tmp_path):
unpicklable_callback = UnpicklableCallback()
with pytest.raises(Exception, match="This class in not picklable"):
joblib.dump(unpicklable_callback, tmp_path / "tmp.joblib")
X, y = make_synthetic_regression()
gbm = lgb.LGBMRegressor(n_estimators=5)
gbm.fit(X, y, callbacks=[unpicklable_callback])
assert gbm.booster_.attr_set_inside_callback == 40
@pytest.mark.parametrize("rng_constructor", [np.random.RandomState, np.random.default_rng])
def test_random_state_object(rng_constructor):
X, y = load_iris(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, random_state=42)
state1 = rng_constructor(123)
state2 = rng_constructor(123)
clf1 = lgb.LGBMClassifier(n_estimators=10, subsample=0.5, subsample_freq=1, random_state=state1)
clf2 = lgb.LGBMClassifier(n_estimators=10, subsample=0.5, subsample_freq=1, random_state=state2)
# Test if random_state is properly stored
assert clf1.random_state is state1
assert clf2.random_state is state2
# Test if two random states produce identical models
clf1.fit(X_train, y_train)
clf2.fit(X_train, y_train)
y_pred1 = clf1.predict(X_test, raw_score=True)
y_pred2 = clf2.predict(X_test, raw_score=True)
np.testing.assert_allclose(y_pred1, y_pred2)
np_assert_array_equal(clf1.feature_importances_, clf2.feature_importances_, strict=True)
df1 = clf1.booster_.model_to_string(num_iteration=0)
df2 = clf2.booster_.model_to_string(num_iteration=0)
assert df1 == df2
# Test if subsequent fits sample from random_state object and produce different models
clf1.fit(X_train, y_train)
y_pred1_refit = clf1.predict(X_test, raw_score=True)
df3 = clf1.booster_.model_to_string(num_iteration=0)
assert clf1.random_state is state1
assert clf2.random_state is state2
with pytest.raises(AssertionError): # noqa: PT011
np.testing.assert_allclose(y_pred1, y_pred1_refit)
assert df1 != df3
def test_feature_importances_single_leaf():
data = load_iris(return_X_y=False)
clf = lgb.LGBMClassifier(n_estimators=10)
clf.fit(data.data, data.target)
importances = clf.feature_importances_
assert len(importances) == 4
def test_feature_importances_type():
data = load_iris(return_X_y=False)
clf = lgb.LGBMClassifier(n_estimators=10)
clf.fit(data.data, data.target)
clf.set_params(importance_type="split")
importances_split = clf.feature_importances_
clf.set_params(importance_type="gain")
importances_gain = clf.feature_importances_
# Test that the largest element is NOT the same, the smallest can be the same, i.e. zero
importance_split_top1 = sorted(importances_split, reverse=True)[0]
importance_gain_top1 = sorted(importances_gain, reverse=True)[0]
assert importance_split_top1 != importance_gain_top1
# why fixed seed?
# sometimes there is no difference how cols are treated (cat or not cat)
def test_pandas_categorical(rng_fixed_seed, tmp_path):
pd = pytest.importorskip("pandas")
X = pd.DataFrame(
{
"A": rng_fixed_seed.permutation(["a", "b", "c", "d"] * 75), # str
"B": rng_fixed_seed.permutation([1, 2, 3] * 100), # int
"C": rng_fixed_seed.permutation([0.1, 0.2, -0.1, -0.1, 0.2] * 60), # float
"D": rng_fixed_seed.permutation([True, False] * 150), # bool
"E": pd.Categorical(rng_fixed_seed.permutation(["z", "y", "x", "w", "v"] * 60), ordered=True),
}
) # str and ordered categorical
y = rng_fixed_seed.permutation([0, 1] * 150)
X_test = pd.DataFrame(
{
"A": rng_fixed_seed.permutation(["a", "b", "e"] * 20), # unseen category
"B": rng_fixed_seed.permutation([1, 3] * 30),
"C": rng_fixed_seed.permutation([0.1, -0.1, 0.2, 0.2] * 15),
"D": rng_fixed_seed.permutation([True, False] * 30),
"E": pd.Categorical(rng_fixed_seed.permutation(["z", "y"] * 30), ordered=True),
}
)
cat_cols_actual = ["A", "B", "C", "D"]
cat_cols_to_store = cat_cols_actual + ["E"]
X[cat_cols_actual] = X[cat_cols_actual].astype("category")
X_test[cat_cols_actual] = X_test[cat_cols_actual].astype("category")
cat_values = [X[col].cat.categories.tolist() for col in cat_cols_to_store]
gbm0 = lgb.sklearn.LGBMClassifier(n_estimators=10).fit(X, y)
pred0 = gbm0.predict(X_test, raw_score=True)
pred_prob = gbm0.predict_proba(X_test)[:, 1]
gbm1 = lgb.sklearn.LGBMClassifier(n_estimators=10).fit(X, pd.Series(y), categorical_feature=[0])
pred1 = gbm1.predict(X_test, raw_score=True)
gbm2 = lgb.sklearn.LGBMClassifier(n_estimators=10).fit(X, y, categorical_feature=["A"])
pred2 = gbm2.predict(X_test, raw_score=True)
gbm3 = lgb.sklearn.LGBMClassifier(n_estimators=10).fit(X, y, categorical_feature=["A", "B", "C", "D"])
pred3 = gbm3.predict(X_test, raw_score=True)
categorical_model_path = tmp_path / "categorical.model"
gbm3.booster_.save_model(categorical_model_path)
gbm4 = lgb.Booster(model_file=categorical_model_path)
pred4 = gbm4.predict(X_test)
gbm5 = lgb.sklearn.LGBMClassifier(n_estimators=10).fit(X, y, categorical_feature=["A", "B", "C", "D", "E"])
pred5 = gbm5.predict(X_test, raw_score=True)
gbm6 = lgb.sklearn.LGBMClassifier(n_estimators=10).fit(X, y, categorical_feature=[])
pred6 = gbm6.predict(X_test, raw_score=True)
with pytest.raises(AssertionError): # noqa: PT011
np.testing.assert_allclose(pred0, pred1)
with pytest.raises(AssertionError): # noqa: PT011
np.testing.assert_allclose(pred0, pred2)
np.testing.assert_allclose(pred1, pred2)
np.testing.assert_allclose(pred0, pred3)
np.testing.assert_allclose(pred_prob, pred4)
with pytest.raises(AssertionError): # noqa: PT011
np.testing.assert_allclose(pred0, pred5) # ordered cat features aren't treated as cat features by default
with pytest.raises(AssertionError): # noqa: PT011
np.testing.assert_allclose(pred0, pred6)
assert gbm0.booster_.pandas_categorical == cat_values
assert gbm1.booster_.pandas_categorical == cat_values
assert gbm2.booster_.pandas_categorical == cat_values
assert gbm3.booster_.pandas_categorical == cat_values
assert gbm4.pandas_categorical == cat_values
assert gbm5.booster_.pandas_categorical == cat_values
assert gbm6.booster_.pandas_categorical == cat_values
def test_pandas_sparse(rng):
pd = pytest.importorskip("pandas")
X = pd.DataFrame(
{
"A": pd.arrays.SparseArray(rng.permutation([0, 1, 2] * 100)),
"B": pd.arrays.SparseArray(rng.permutation([0.0, 0.1, 0.2, -0.1, 0.2] * 60)),
"C": pd.arrays.SparseArray(rng.permutation([True, False] * 150)),
}
)
y = pd.Series(pd.arrays.SparseArray(rng.permutation([0, 1] * 150)))
X_test = pd.DataFrame(
{
"A": pd.arrays.SparseArray(rng.permutation([0, 2] * 30)),
"B": pd.arrays.SparseArray(rng.permutation([0.0, 0.1, 0.2, -0.1] * 15)),
"C": pd.arrays.SparseArray(rng.permutation([True, False] * 30)),
}
)
for dtype in pd.concat([X.dtypes, X_test.dtypes, pd.Series(y.dtypes)]):
assert isinstance(dtype, pd.SparseDtype)
gbm = lgb.sklearn.LGBMClassifier(n_estimators=10).fit(X, y)
pred_sparse = gbm.predict(X_test, raw_score=True)
if hasattr(X_test, "sparse"):
pred_dense = gbm.predict(X_test.sparse.to_dense(), raw_score=True)
else:
pred_dense = gbm.predict(X_test.to_dense(), raw_score=True)
np.testing.assert_allclose(pred_sparse, pred_dense)
def test_predict():
# With default params
iris = load_iris(return_X_y=False)
X_train, X_test, y_train, _ = train_test_split(iris.data, iris.target, test_size=0.2, random_state=42)
gbm = lgb.train({"objective": "multiclass", "num_class": 3, "verbose": -1}, lgb.Dataset(X_train, y_train))
clf = lgb.LGBMClassifier(verbose=-1).fit(X_train, y_train)
# Tests same probabilities
res_engine = gbm.predict(X_test)
res_sklearn = clf.predict_proba(X_test)
np.testing.assert_allclose(res_engine, res_sklearn)
# Tests same predictions
res_engine = np.argmax(gbm.predict(X_test), axis=1)
res_sklearn = clf.predict(X_test)
np.testing.assert_equal(res_engine, res_sklearn)
# Tests same raw scores
res_engine = gbm.predict(X_test, raw_score=True)
res_sklearn = clf.predict(X_test, raw_score=True)
np.testing.assert_allclose(res_engine, res_sklearn)
# Tests same leaf indices
res_engine = gbm.predict(X_test, pred_leaf=True)
res_sklearn = clf.predict(X_test, pred_leaf=True)
np.testing.assert_equal(res_engine, res_sklearn)
# Tests same feature contributions
res_engine = gbm.predict(X_test, pred_contrib=True)
res_sklearn = clf.predict(X_test, pred_contrib=True)
np.testing.assert_allclose(res_engine, res_sklearn)
# Tests other parameters for the prediction works
res_engine = gbm.predict(X_test)
res_sklearn_params = clf.predict_proba(X_test, pred_early_stop=True, pred_early_stop_margin=1.0)
with pytest.raises(AssertionError): # noqa: PT011
np.testing.assert_allclose(res_engine, res_sklearn_params)
# Tests start_iteration
# Tests same probabilities, starting from iteration 10
res_engine = gbm.predict(X_test, start_iteration=10)
res_sklearn = clf.predict_proba(X_test, start_iteration=10)
np.testing.assert_allclose(res_engine, res_sklearn)
# Tests same predictions, starting from iteration 10
res_engine = np.argmax(gbm.predict(X_test, start_iteration=10), axis=1)
res_sklearn = clf.predict(X_test, start_iteration=10)
np.testing.assert_equal(res_engine, res_sklearn)
# Tests same raw scores, starting from iteration 10
res_engine = gbm.predict(X_test, raw_score=True, start_iteration=10)
res_sklearn = clf.predict(X_test, raw_score=True, start_iteration=10)
np.testing.assert_allclose(res_engine, res_sklearn)
# Tests same leaf indices, starting from iteration 10
res_engine = gbm.predict(X_test, pred_leaf=True, start_iteration=10)
res_sklearn = clf.predict(X_test, pred_leaf=True, start_iteration=10)
np.testing.assert_equal(res_engine, res_sklearn)
# Tests same feature contributions, starting from iteration 10
res_engine = gbm.predict(X_test, pred_contrib=True, start_iteration=10)
res_sklearn = clf.predict(X_test, pred_contrib=True, start_iteration=10)
np.testing.assert_allclose(res_engine, res_sklearn)
# Tests other parameters for the prediction works, starting from iteration 10
res_engine = gbm.predict(X_test, start_iteration=10)
res_sklearn_params = clf.predict_proba(X_test, pred_early_stop=True, pred_early_stop_margin=1.0, start_iteration=10)
with pytest.raises(AssertionError): # noqa: PT011
np.testing.assert_allclose(res_engine, res_sklearn_params)
# Test multiclass binary classification
num_samples = 100
num_classes = 2
X_train = np.linspace(start=0, stop=10, num=num_samples * 3).reshape(num_samples, 3)
y_train = np.concatenate([np.zeros(int(num_samples / 2 - 10)), np.ones(int(num_samples / 2 + 10))])
gbm = lgb.train({"objective": "multiclass", "num_class": num_classes, "verbose": -1}, lgb.Dataset(X_train, y_train))
clf = lgb.LGBMClassifier(objective="multiclass", num_classes=num_classes).fit(X_train, y_train)
res_engine = gbm.predict(X_train)
res_sklearn = clf.predict_proba(X_train)
assert res_engine.shape == (num_samples, num_classes)
assert res_sklearn.shape == (num_samples, num_classes)
np.testing.assert_allclose(res_engine, res_sklearn)
res_class_sklearn = clf.predict(X_train)
np.testing.assert_allclose(res_class_sklearn, y_train)
def test_decision_function_and_predict_proba_consistency():
# binary
X, y = load_breast_cancer(return_X_y=True)
X_train, X_test, y_train, _ = train_test_split(X, y, test_size=0.2, random_state=42)
clf = lgb.LGBMClassifier(n_estimators=10, random_state=42, verbose=-1).fit(X_train, y_train)
preds_raw = clf.decision_function(X_test)
np.testing.assert_allclose(preds_raw, clf.predict(X_test, raw_score=True))
np.testing.assert_allclose(logistic_sigmoid(preds_raw), clf.predict_proba(X_test)[:, 1])
# multiclass
X, y = load_iris(return_X_y=True)
X_train, X_test, y_train, _ = train_test_split(X, y, test_size=0.2, random_state=42)
clf = lgb.LGBMClassifier(n_estimators=10, random_state=42, verbose=-1).fit(X_train, y_train)
preds_raw = clf.decision_function(X_test)
np.testing.assert_allclose(preds_raw, clf.predict(X_test, raw_score=True))
np.testing.assert_allclose(softmax(preds_raw), clf.predict_proba(X_test))
@pytest.mark.parametrize("method", ["sigmoid", "isotonic"])
def test_calibrated_classifier_cv(method):
# binary
X, y = load_breast_cancer(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
deterministic_params = {
"deterministic": True,
"force_col_wise": True,
"n_jobs": 1,
"seed": 312,
}
clf = CalibratedClassifierCV(
lgb.LGBMClassifier(n_estimators=10, verbose=-1, **deterministic_params),
method=method,
cv=3,
)
clf.fit(X_train, y_train)
proba = clf.predict_proba(X_test)
assert proba.shape == (X_test.shape[0], 2)
np.testing.assert_array_less(proba, 1.0 + 1e-9)
np.testing.assert_array_less(-1e-9, proba)
np.testing.assert_allclose(proba.sum(axis=1), 1.0)
score = accuracy_score(y_test, clf.predict(X_test))
assert 0.8 <= score <= 1.0
# multiclass
X, y = load_iris(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
clf = CalibratedClassifierCV(
lgb.LGBMClassifier(n_estimators=10, verbose=-1, **deterministic_params),
method=method,
cv=3,
)
clf.fit(X_train, y_train)
proba = clf.predict_proba(X_test)
assert proba.shape == (X_test.shape[0], 3)
np.testing.assert_array_less(proba, 1.0 + 1e-9)
np.testing.assert_array_less(-1e-9, proba)
np.testing.assert_allclose(proba.sum(axis=1), 1.0)
score = accuracy_score(y_test, clf.predict(X_test))
assert 0.8 <= score <= 1.0
def test_predict_with_params_from_init():
X, y = load_iris(return_X_y=True)
X_train, X_test, y_train, _ = train_test_split(X, y, test_size=0.2, random_state=42)
predict_params = {"pred_early_stop": True, "pred_early_stop_margin": 1.0}
y_preds_no_params = lgb.LGBMClassifier(verbose=-1).fit(X_train, y_train).predict(X_test, raw_score=True)
y_preds_params_in_predict = (
lgb.LGBMClassifier(verbose=-1).fit(X_train, y_train).predict(X_test, raw_score=True, **predict_params)
)
with pytest.raises(AssertionError): # noqa: PT011
np.testing.assert_allclose(y_preds_no_params, y_preds_params_in_predict)
y_preds_params_in_set_params_before_fit = (
lgb.LGBMClassifier(verbose=-1)
.set_params(**predict_params)
.fit(X_train, y_train)
.predict(X_test, raw_score=True)
)
np.testing.assert_allclose(y_preds_params_in_predict, y_preds_params_in_set_params_before_fit)
y_preds_params_in_set_params_after_fit = (
lgb.LGBMClassifier(verbose=-1)
.fit(X_train, y_train)
.set_params(**predict_params)
.predict(X_test, raw_score=True)
)
np.testing.assert_allclose(y_preds_params_in_predict, y_preds_params_in_set_params_after_fit)
y_preds_params_in_init = (
lgb.LGBMClassifier(verbose=-1, **predict_params).fit(X_train, y_train).predict(X_test, raw_score=True)
)
np.testing.assert_allclose(y_preds_params_in_predict, y_preds_params_in_init)
# test that params passed in predict have higher priority
y_preds_params_overwritten = (
lgb.LGBMClassifier(verbose=-1, **predict_params)
.fit(X_train, y_train)
.predict(X_test, raw_score=True, pred_early_stop=False)
)
np.testing.assert_allclose(y_preds_no_params, y_preds_params_overwritten)
def test_evaluate_train_set():
X, y = make_synthetic_regression()
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, random_state=42)
gbm = lgb.LGBMRegressor(n_estimators=10, verbose=-1)
gbm.fit(X_train, y_train, eval_set=[(X_train, y_train), (X_test, y_test)])
assert len(gbm.evals_result_) == 2
assert "training" in gbm.evals_result_
assert len(gbm.evals_result_["training"]) == 1
assert "l2" in gbm.evals_result_["training"]
assert "valid_1" in gbm.evals_result_
assert len(gbm.evals_result_["valid_1"]) == 1
assert "l2" in gbm.evals_result_["valid_1"]
def test_metrics():
X, y = make_synthetic_regression()
y = abs(y)
params = {"n_estimators": 2, "verbose": -1}
params_fit = {"X": X, "y": y, "eval_set": (X, y)}
# no custom objective, no custom metric
# default metric
gbm = lgb.LGBMRegressor(**params).fit(**params_fit)
assert len(gbm.evals_result_["training"]) == 1
assert "l2" in gbm.evals_result_["training"]
# non-default metric
gbm = lgb.LGBMRegressor(metric="mape", **params).fit(**params_fit)
assert len(gbm.evals_result_["training"]) == 1
assert "mape" in gbm.evals_result_["training"]
# no metric
gbm = lgb.LGBMRegressor(metric="None", **params).fit(**params_fit)
assert gbm.evals_result_ == {}
# non-default metric in eval_metric
gbm = lgb.LGBMRegressor(**params).fit(eval_metric="mape", **params_fit)
assert len(gbm.evals_result_["training"]) == 2
assert "l2" in gbm.evals_result_["training"]
assert "mape" in gbm.evals_result_["training"]
# non-default metric with non-default metric in eval_metric
gbm = lgb.LGBMRegressor(metric="gamma", **params).fit(eval_metric="mape", **params_fit)
assert len(gbm.evals_result_["training"]) == 2
assert "gamma" in gbm.evals_result_["training"]
assert "mape" in gbm.evals_result_["training"]
# non-default metric with multiple metrics in eval_metric
gbm = lgb.LGBMRegressor(metric="gamma", **params).fit(eval_metric=["l2", "mape"], **params_fit)
assert len(gbm.evals_result_["training"]) == 3
assert "gamma" in gbm.evals_result_["training"]
assert "l2" in gbm.evals_result_["training"]
assert "mape" in gbm.evals_result_["training"]
# non-default metric with multiple metrics in eval_metric for LGBMClassifier
X_classification, y_classification = load_breast_cancer(return_X_y=True)
params_classification = {"n_estimators": 2, "verbose": -1, "objective": "binary", "metric": "binary_logloss"}
params_fit_classification = {
"X": X_classification,
"y": y_classification,
"eval_set": (X_classification, y_classification),
}
gbm = lgb.LGBMClassifier(**params_classification).fit(eval_metric=["fair", "error"], **params_fit_classification)
assert len(gbm.evals_result_["training"]) == 3
assert "fair" in gbm.evals_result_["training"]
assert "binary_error" in gbm.evals_result_["training"]
assert "binary_logloss" in gbm.evals_result_["training"]
# default metric for non-default objective
gbm = lgb.LGBMRegressor(objective="regression_l1", **params).fit(**params_fit)
assert len(gbm.evals_result_["training"]) == 1
assert "l1" in gbm.evals_result_["training"]
# non-default metric for non-default objective
gbm = lgb.LGBMRegressor(objective="regression_l1", metric="mape", **params).fit(**params_fit)
assert len(gbm.evals_result_["training"]) == 1
assert "mape" in gbm.evals_result_["training"]
# no metric
gbm = lgb.LGBMRegressor(objective="regression_l1", metric="None", **params).fit(**params_fit)
assert gbm.evals_result_ == {}
# non-default metric in eval_metric for non-default objective
gbm = lgb.LGBMRegressor(objective="regression_l1", **params).fit(eval_metric="mape", **params_fit)
assert len(gbm.evals_result_["training"]) == 2
assert "l1" in gbm.evals_result_["training"]
assert "mape" in gbm.evals_result_["training"]
# non-default metric with non-default metric in eval_metric for non-default objective
gbm = lgb.LGBMRegressor(objective="regression_l1", metric="gamma", **params).fit(eval_metric="mape", **params_fit)
assert len(gbm.evals_result_["training"]) == 2
assert "gamma" in gbm.evals_result_["training"]
assert "mape" in gbm.evals_result_["training"]
# non-default metric with multiple metrics in eval_metric for non-default objective
gbm = lgb.LGBMRegressor(objective="regression_l1", metric="gamma", **params).fit(
eval_metric=["l2", "mape"], **params_fit
)
assert len(gbm.evals_result_["training"]) == 3
assert "gamma" in gbm.evals_result_["training"]
assert "l2" in gbm.evals_result_["training"]
assert "mape" in gbm.evals_result_["training"]
# custom objective, no custom metric
# default regression metric for custom objective
gbm = lgb.LGBMRegressor(objective=custom_dummy_obj, **params).fit(**params_fit)
assert len(gbm.evals_result_["training"]) == 1
assert "l2" in gbm.evals_result_["training"]
# non-default regression metric for custom objective
gbm = lgb.LGBMRegressor(objective=custom_dummy_obj, metric="mape", **params).fit(**params_fit)
assert len(gbm.evals_result_["training"]) == 1
assert "mape" in gbm.evals_result_["training"]
# multiple regression metrics for custom objective
gbm = lgb.LGBMRegressor(objective=custom_dummy_obj, metric=["l1", "gamma"], **params).fit(**params_fit)
assert len(gbm.evals_result_["training"]) == 2
assert "l1" in gbm.evals_result_["training"]
assert "gamma" in gbm.evals_result_["training"]
# no metric
gbm = lgb.LGBMRegressor(objective=custom_dummy_obj, metric="None", **params).fit(**params_fit)
assert gbm.evals_result_ == {}
# default regression metric with non-default metric in eval_metric for custom objective
gbm = lgb.LGBMRegressor(objective=custom_dummy_obj, **params).fit(eval_metric="mape", **params_fit)
assert len(gbm.evals_result_["training"]) == 2
assert "l2" in gbm.evals_result_["training"]
assert "mape" in gbm.evals_result_["training"]
# non-default regression metric with metric in eval_metric for custom objective
gbm = lgb.LGBMRegressor(objective=custom_dummy_obj, metric="mape", **params).fit(eval_metric="gamma", **params_fit)
assert len(gbm.evals_result_["training"]) == 2
assert "mape" in gbm.evals_result_["training"]
assert "gamma" in gbm.evals_result_["training"]
# multiple regression metrics with metric in eval_metric for custom objective
gbm = lgb.LGBMRegressor(objective=custom_dummy_obj, metric=["l1", "gamma"], **params).fit(
eval_metric="l2", **params_fit
)
assert len(gbm.evals_result_["training"]) == 3
assert "l1" in gbm.evals_result_["training"]
assert "gamma" in gbm.evals_result_["training"]
assert "l2" in gbm.evals_result_["training"]
# multiple regression metrics with multiple metrics in eval_metric for custom objective
gbm = lgb.LGBMRegressor(objective=custom_dummy_obj, metric=["l1", "gamma"], **params).fit(
eval_metric=["l2", "mape"], **params_fit
)
assert len(gbm.evals_result_["training"]) == 4
assert "l1" in gbm.evals_result_["training"]
assert "gamma" in gbm.evals_result_["training"]
assert "l2" in gbm.evals_result_["training"]
assert "mape" in gbm.evals_result_["training"]
# no custom objective, custom metric
# default metric with custom metric
gbm = lgb.LGBMRegressor(**params).fit(eval_metric=constant_metric, **params_fit)
assert len(gbm.evals_result_["training"]) == 2
assert "l2" in gbm.evals_result_["training"]
assert "error" in gbm.evals_result_["training"]
# non-default metric with custom metric
gbm = lgb.LGBMRegressor(metric="mape", **params).fit(eval_metric=constant_metric, **params_fit)
assert len(gbm.evals_result_["training"]) == 2
assert "mape" in gbm.evals_result_["training"]
assert "error" in gbm.evals_result_["training"]
# multiple metrics with custom metric
gbm = lgb.LGBMRegressor(metric=["l1", "gamma"], **params).fit(eval_metric=constant_metric, **params_fit)
assert len(gbm.evals_result_["training"]) == 3
assert "l1" in gbm.evals_result_["training"]
assert "gamma" in gbm.evals_result_["training"]
assert "error" in gbm.evals_result_["training"]
# custom metric (disable default metric)
gbm = lgb.LGBMRegressor(metric="None", **params).fit(eval_metric=constant_metric, **params_fit)
assert len(gbm.evals_result_["training"]) == 1
assert "error" in gbm.evals_result_["training"]
# default metric for non-default objective with custom metric
gbm = lgb.LGBMRegressor(objective="regression_l1", **params).fit(eval_metric=constant_metric, **params_fit)
assert len(gbm.evals_result_["training"]) == 2
assert "l1" in gbm.evals_result_["training"]
assert "error" in gbm.evals_result_["training"]
# non-default metric for non-default objective with custom metric
gbm = lgb.LGBMRegressor(objective="regression_l1", metric="mape", **params).fit(
eval_metric=constant_metric, **params_fit
)
assert len(gbm.evals_result_["training"]) == 2
assert "mape" in gbm.evals_result_["training"]
assert "error" in gbm.evals_result_["training"]
# multiple metrics for non-default objective with custom metric
gbm = lgb.LGBMRegressor(objective="regression_l1", metric=["l1", "gamma"], **params).fit(
eval_metric=constant_metric, **params_fit
)
assert len(gbm.evals_result_["training"]) == 3
assert "l1" in gbm.evals_result_["training"]
assert "gamma" in gbm.evals_result_["training"]
assert "error" in gbm.evals_result_["training"]
# custom metric (disable default metric for non-default objective)
gbm = lgb.LGBMRegressor(objective="regression_l1", metric="None", **params).fit(
eval_metric=constant_metric, **params_fit
)
assert len(gbm.evals_result_["training"]) == 1
assert "error" in gbm.evals_result_["training"]
# custom objective, custom metric
# custom metric for custom objective
gbm = lgb.LGBMRegressor(objective=custom_dummy_obj, **params).fit(eval_metric=constant_metric, **params_fit)
assert len(gbm.evals_result_["training"]) == 2
assert "error" in gbm.evals_result_["training"]
# non-default regression metric with custom metric for custom objective
gbm = lgb.LGBMRegressor(objective=custom_dummy_obj, metric="mape", **params).fit(
eval_metric=constant_metric, **params_fit
)
assert len(gbm.evals_result_["training"]) == 2
assert "mape" in gbm.evals_result_["training"]
assert "error" in gbm.evals_result_["training"]
# multiple regression metrics with custom metric for custom objective
gbm = lgb.LGBMRegressor(objective=custom_dummy_obj, metric=["l2", "mape"], **params).fit(
eval_metric=constant_metric, **params_fit
)
assert len(gbm.evals_result_["training"]) == 3
assert "l2" in gbm.evals_result_["training"]
assert "mape" in gbm.evals_result_["training"]
assert "error" in gbm.evals_result_["training"]
X, y = load_digits(n_class=3, return_X_y=True)
params_fit = {"X": X, "y": y, "eval_set": (X, y)}
# default metric and invalid binary metric is replaced with multiclass alternative
gbm = lgb.LGBMClassifier(**params).fit(eval_metric="binary_error", **params_fit)
assert len(gbm.evals_result_["training"]) == 2
assert "multi_logloss" in gbm.evals_result_["training"]
assert "multi_error" in gbm.evals_result_["training"]
# invalid binary metric is replaced with multiclass alternative
gbm = lgb.LGBMClassifier(**params).fit(eval_metric="binary_error", **params_fit)
assert gbm.objective_ == "multiclass"
assert len(gbm.evals_result_["training"]) == 2
assert "multi_logloss" in gbm.evals_result_["training"]
assert "multi_error" in gbm.evals_result_["training"]
# default metric for non-default multiclass objective
# and invalid binary metric is replaced with multiclass alternative
gbm = lgb.LGBMClassifier(objective="ovr", **params).fit(eval_metric="binary_error", **params_fit)
assert gbm.objective_ == "ovr"
assert len(gbm.evals_result_["training"]) == 2
assert "multi_logloss" in gbm.evals_result_["training"]
assert "multi_error" in gbm.evals_result_["training"]
X, y = load_digits(n_class=2, return_X_y=True)
params_fit = {"X": X, "y": y, "eval_set": (X, y)}
# default metric and invalid multiclass metric is replaced with binary alternative
gbm = lgb.LGBMClassifier(**params).fit(eval_metric="multi_error", **params_fit)
assert len(gbm.evals_result_["training"]) == 2
assert "binary_logloss" in gbm.evals_result_["training"]
assert "binary_error" in gbm.evals_result_["training"]
# invalid multiclass metric is replaced with binary alternative for custom objective
gbm = lgb.LGBMClassifier(objective=custom_dummy_obj, **params).fit(eval_metric="multi_logloss", **params_fit)
assert len(gbm.evals_result_["training"]) == 1
assert "binary_logloss" in gbm.evals_result_["training"]
# the evaluation metric changes to multiclass metric even num classes is 2 for multiclass objective
gbm = lgb.LGBMClassifier(objective="multiclass", num_classes=2, **params).fit(
eval_metric="binary_logloss", **params_fit
)
assert len(gbm._evals_result["training"]) == 1
assert "multi_logloss" in gbm.evals_result_["training"]
# the evaluation metric changes to multiclass metric even num classes is 2 for ovr objective
gbm = lgb.LGBMClassifier(objective="ovr", num_classes=2, **params).fit(eval_metric="binary_error", **params_fit)
assert gbm.objective_ == "ovr"
assert len(gbm.evals_result_["training"]) == 2
assert "multi_logloss" in gbm.evals_result_["training"]
assert "multi_error" in gbm.evals_result_["training"]
def test_multiple_eval_metrics():
X, y = load_breast_cancer(return_X_y=True)
params = {"n_estimators": 2, "verbose": -1, "objective": "binary", "metric": "binary_logloss"}
params_fit = {"X": X, "y": y, "eval_set": (X, y)}
# Verify that can receive a list of metrics, only callable
gbm = lgb.LGBMClassifier(**params).fit(eval_metric=[constant_metric, decreasing_metric], **params_fit)
assert len(gbm.evals_result_["training"]) == 3
assert "error" in gbm.evals_result_["training"]
assert "decreasing_metric" in gbm.evals_result_["training"]
assert "binary_logloss" in gbm.evals_result_["training"]
# Verify that can receive a list of custom and built-in metrics
gbm = lgb.LGBMClassifier(**params).fit(eval_metric=[constant_metric, decreasing_metric, "fair"], **params_fit)
assert len(gbm.evals_result_["training"]) == 4
assert "error" in gbm.evals_result_["training"]
assert "decreasing_metric" in gbm.evals_result_["training"]
assert "binary_logloss" in gbm.evals_result_["training"]
assert "fair" in gbm.evals_result_["training"]
# Verify that works as expected when eval_metric is empty
gbm = lgb.LGBMClassifier(**params).fit(eval_metric=[], **params_fit)
assert len(gbm.evals_result_["training"]) == 1
assert "binary_logloss" in gbm.evals_result_["training"]
# Verify that can receive a list of metrics, only built-in
gbm = lgb.LGBMClassifier(**params).fit(eval_metric=["fair", "error"], **params_fit)
assert len(gbm.evals_result_["training"]) == 3
assert "binary_logloss" in gbm.evals_result_["training"]
# Verify that eval_metric is robust to receiving a list with None
gbm = lgb.LGBMClassifier(**params).fit(eval_metric=["fair", "error", None], **params_fit)
assert len(gbm.evals_result_["training"]) == 3
assert "binary_logloss" in gbm.evals_result_["training"]
def test_nan_handle(rng):
nrows = 100
ncols = 10
X = rng.standard_normal(size=(nrows, ncols))
y = rng.standard_normal(size=(nrows,)) + np.full(nrows, 1e30)
weight = np.zeros(nrows)
params = {"n_estimators": 20, "verbose": -1}
params_fit = {"X": X, "y": y, "sample_weight": weight, "eval_set": (X, y), "callbacks": [lgb.early_stopping(5)]}
gbm = lgb.LGBMRegressor(**params).fit(**params_fit)
np.testing.assert_allclose(gbm.evals_result_["training"]["l2"], np.nan)
@pytest.mark.skipif(BuildInfo.has_cuda, reason="Skip due to differences in implementation details of CUDA version")
def test_first_metric_only():
def fit_and_check(eval_set_names, metric_names, assumed_iteration, first_metric_only):
params["first_metric_only"] = first_metric_only
gbm = lgb.LGBMRegressor(**params).fit(**params_fit)
assert len(gbm.evals_result_) == len(eval_set_names)
for eval_set_name in eval_set_names:
assert eval_set_name in gbm.evals_result_
assert len(gbm.evals_result_[eval_set_name]) == len(metric_names)
for metric_name in metric_names:
assert metric_name in gbm.evals_result_[eval_set_name]
actual = len(gbm.evals_result_[eval_set_name][metric_name])
expected = assumed_iteration + (
params["early_stopping_rounds"]
if eval_set_name != "training" and assumed_iteration != gbm.n_estimators
else 0
)
assert expected == actual
if eval_set_name != "training":
assert assumed_iteration == gbm.best_iteration_
else:
assert gbm.n_estimators == gbm.best_iteration_
X, y = make_synthetic_regression(n_samples=300)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
X_test1, X_test2, y_test1, y_test2 = train_test_split(X_test, y_test, test_size=0.5, random_state=72)
params = {
"n_estimators": 30,
"learning_rate": 0.8,
"num_leaves": 15,
"verbose": -1,
"seed": 123,
"early_stopping_rounds": 5,
} # early stop should be supported via global LightGBM parameter
params_fit = {"X": X_train, "y": y_train}
iter_valid1_l1 = 4
iter_valid1_l2 = 4
iter_valid2_l1 = 2
iter_valid2_l2 = 2
assert len({iter_valid1_l1, iter_valid1_l2, iter_valid2_l1, iter_valid2_l2}) == 2
iter_min_l1 = min([iter_valid1_l1, iter_valid2_l1])
iter_min_l2 = min([iter_valid1_l2, iter_valid2_l2])
iter_min = min([iter_min_l1, iter_min_l2])
iter_min_valid1 = min([iter_valid1_l1, iter_valid1_l2])
# feval
params["metric"] = "None"
params_fit["eval_metric"] = lambda preds, train_data: [
decreasing_metric(preds, train_data),
constant_metric(preds, train_data),
]
params_fit["eval_set"] = (X_test1, y_test1)
fit_and_check(["valid_0"], ["decreasing_metric", "error"], 1, False)
fit_and_check(["valid_0"], ["decreasing_metric", "error"], 30, True)
params_fit["eval_metric"] = lambda preds, train_data: [
constant_metric(preds, train_data),
decreasing_metric(preds, train_data),
]
fit_and_check(["valid_0"], ["decreasing_metric", "error"], 1, True)
# single eval_set
params.pop("metric")
params_fit.pop("eval_metric")
fit_and_check(["valid_0"], ["l2"], iter_valid1_l2, False)
fit_and_check(["valid_0"], ["l2"], iter_valid1_l2, True)
params_fit["eval_metric"] = "l2"
fit_and_check(["valid_0"], ["l2"], iter_valid1_l2, False)
fit_and_check(["valid_0"], ["l2"], iter_valid1_l2, True)
params_fit["eval_metric"] = "l1"
fit_and_check(["valid_0"], ["l1", "l2"], iter_min_valid1, False)
fit_and_check(["valid_0"], ["l1", "l2"], iter_valid1_l1, True)
params_fit["eval_metric"] = ["l1", "l2"]
fit_and_check(["valid_0"], ["l1", "l2"], iter_min_valid1, False)
fit_and_check(["valid_0"], ["l1", "l2"], iter_valid1_l1, True)
params_fit["eval_metric"] = ["l2", "l1"]
fit_and_check(["valid_0"], ["l1", "l2"], iter_min_valid1, False)
fit_and_check(["valid_0"], ["l1", "l2"], iter_valid1_l2, True)
params_fit["eval_metric"] = ["l2", "regression", "mse"] # test aliases
fit_and_check(["valid_0"], ["l2"], iter_valid1_l2, False)
fit_and_check(["valid_0"], ["l2"], iter_valid1_l2, True)
# two eval_set
params_fit["eval_set"] = [(X_test1, y_test1), (X_test2, y_test2)]
params_fit["eval_metric"] = ["l1", "l2"]
fit_and_check(["valid_0", "valid_1"], ["l1", "l2"], iter_min_l1, True)
params_fit["eval_metric"] = ["l2", "l1"]
fit_and_check(["valid_0", "valid_1"], ["l1", "l2"], iter_min_l2, True)
params_fit["eval_set"] = [(X_test2, y_test2), (X_test1, y_test1)]
params_fit["eval_metric"] = ["l1", "l2"]
fit_and_check(["valid_0", "valid_1"], ["l1", "l2"], iter_min, False)
fit_and_check(["valid_0", "valid_1"], ["l1", "l2"], iter_min_l1, True)
params_fit["eval_metric"] = ["l2", "l1"]
fit_and_check(["valid_0", "valid_1"], ["l1", "l2"], iter_min, False)
fit_and_check(["valid_0", "valid_1"], ["l1", "l2"], iter_min_l2, True)
def test_class_weight():
X, y = load_digits(n_class=10, return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
y_train_str = y_train.astype("str")
y_test_str = y_test.astype("str")
gbm = lgb.LGBMClassifier(n_estimators=10, class_weight="balanced", verbose=-1)
gbm.fit(
X_train,
y_train,
eval_set=[(X_train, y_train), (X_test, y_test), (X_test, y_test), (X_test, y_test), (X_test, y_test)],
eval_class_weight=["balanced", None, "balanced", {1: 10, 4: 20}, {5: 30, 2: 40}],
)
for eval_set1, eval_set2 in itertools.combinations(gbm.evals_result_.keys(), 2):
for metric in gbm.evals_result_[eval_set1]:
np.testing.assert_raises(
AssertionError,
np.testing.assert_allclose,
gbm.evals_result_[eval_set1][metric],
gbm.evals_result_[eval_set2][metric],
)
gbm_str = lgb.LGBMClassifier(n_estimators=10, class_weight="balanced", verbose=-1)
gbm_str.fit(
X_train,
y_train_str,
eval_set=[
(X_train, y_train_str),
(X_test, y_test_str),
(X_test, y_test_str),
(X_test, y_test_str),
(X_test, y_test_str),
],
eval_class_weight=["balanced", None, "balanced", {"1": 10, "4": 20}, {"5": 30, "2": 40}],
)
for eval_set1, eval_set2 in itertools.combinations(gbm_str.evals_result_.keys(), 2):
for metric in gbm_str.evals_result_[eval_set1]:
np.testing.assert_raises(
AssertionError,
np.testing.assert_allclose,
gbm_str.evals_result_[eval_set1][metric],
gbm_str.evals_result_[eval_set2][metric],
)
for eval_set in gbm.evals_result_:
for metric in gbm.evals_result_[eval_set]:
np.testing.assert_allclose(gbm.evals_result_[eval_set][metric], gbm_str.evals_result_[eval_set][metric])
def test_continue_training_with_model():
X, y = load_digits(n_class=3, return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, random_state=42)
init_gbm = lgb.LGBMClassifier(n_estimators=5).fit(X_train, y_train, eval_set=(X_test, y_test))
gbm = lgb.LGBMClassifier(n_estimators=5).fit(X_train, y_train, eval_set=(X_test, y_test), init_model=init_gbm)
assert len(init_gbm.evals_result_["valid_0"]["multi_logloss"]) == len(gbm.evals_result_["valid_0"]["multi_logloss"])
assert len(init_gbm.evals_result_["valid_0"]["multi_logloss"]) == 5
assert gbm.evals_result_["valid_0"]["multi_logloss"][-1] < init_gbm.evals_result_["valid_0"]["multi_logloss"][-1]
def test_booster_does_not_hold_datasets():
"""fit() should clear the Dataset objects stored on the Booster"""
data = load_iris(return_X_y=False)
clf = lgb.LGBMClassifier(n_estimators=2, max_depth=2)
clf.fit(data.data, data.target)
assert not hasattr(clf.booster_, "train_set")
assert not hasattr(clf.booster_, "valid_sets")
def test_actual_number_of_trees():
X = [[1, 2, 3], [1, 2, 3]]
y = [1.0, 1.0]
n_estimators = 5
gbm = lgb.LGBMRegressor(n_estimators=n_estimators).fit(X, y)
assert gbm.n_estimators == n_estimators
assert gbm.n_estimators_ == 1
assert gbm.n_iter_ == 1
np_assert_array_equal(gbm.predict(np.array(X) * 10), y, strict=True)
def test_check_is_fitted():
X, y = load_digits(n_class=2, return_X_y=True)
est = lgb.LGBMModel(n_estimators=5, objective="binary")
clf = lgb.LGBMClassifier(n_estimators=5)
reg = lgb.LGBMRegressor(n_estimators=5)
rnk = lgb.LGBMRanker(n_estimators=5)
models = (est, clf, reg, rnk)
for model in models:
err_msg = f"This {type(model).__name__} instance is not fitted yet. Call 'fit' with appropriate arguments before using this estimator."
with pytest.raises(lgb.compat.LGBMNotFittedError, match=err_msg):
check_is_fitted(model)
est.fit(X, y)
clf.fit(X, y)
reg.fit(X, y)
rnk.fit(X, y, group=np.ones(X.shape[0]))
for model in models:
check_is_fitted(model)
@pytest.mark.parametrize("estimator_class", estimator_classes)
@pytest.mark.parametrize("max_depth", [3, 4, 5, 8])
def test_max_depth_warning_is_never_raised(capsys, estimator_class, max_depth):
X, y = make_blobs(n_samples=1_000, n_features=1, centers=2)
params = {"n_estimators": 1, "max_depth": max_depth, "verbose": 0}
if estimator_class is lgb.LGBMModel:
estimator_class(**{**params, "objective": "binary"}).fit(X, y)
elif estimator_class is lgb.LGBMRanker:
estimator_class(**params).fit(X, y, group=np.ones(X.shape[0]))
else:
estimator_class(**params).fit(X, y)
assert "Provided parameters constrain tree depth" not in capsys.readouterr().out
def test_verbosity_is_respected_when_using_custom_objective(capsys):
X, y = make_synthetic_regression()
params = {
"objective": objective_ls,
"nonsense": 123,
"num_leaves": 3,
}
lgb.LGBMRegressor(**params, verbosity=-1, n_estimators=1).fit(X, y)
assert capsys.readouterr().out == ""
lgb.LGBMRegressor(**params, verbosity=0, n_estimators=1).fit(X, y)
assert "[LightGBM] [Warning] Unknown parameter: nonsense" in capsys.readouterr().out
def test_fit_only_raises_num_rounds_warning_when_expected(capsys):
X, y = make_synthetic_regression()
base_kwargs = {
"num_leaves": 5,
"verbosity": -1,
}
# no warning: no aliases, all defaults
reg = lgb.LGBMRegressor(**base_kwargs).fit(X, y)
assert reg.n_estimators_ == 100
assert_silent(capsys)
# no warning: no aliases, just n_estimators
reg = lgb.LGBMRegressor(**base_kwargs, n_estimators=2).fit(X, y)
assert reg.n_estimators_ == 2
assert_silent(capsys)
# no warning: 1 alias + n_estimators (both same value)
reg = lgb.LGBMRegressor(**base_kwargs, n_estimators=3, n_iter=3).fit(X, y)
assert reg.n_estimators_ == 3
assert_silent(capsys)
# no warning: 1 alias + n_estimators (different values... value from params should win)
reg = lgb.LGBMRegressor(**base_kwargs, n_estimators=3, n_iter=4).fit(X, y)
assert reg.n_estimators_ == 4
assert_silent(capsys)
# no warning: 2 aliases (both same value)
reg = lgb.LGBMRegressor(**base_kwargs, n_iter=3, num_iterations=3).fit(X, y)
assert reg.n_estimators_ == 3
assert_silent(capsys)
# no warning: 4 aliases (all same value)
reg = lgb.LGBMRegressor(**base_kwargs, n_iter=3, num_trees=3, nrounds=3, max_iter=3).fit(X, y)
assert reg.n_estimators_ == 3
assert_silent(capsys)
# warning: 2 aliases (different values... "num_iterations" wins because it's the main param name)
with pytest.warns(UserWarning, match="LightGBM will perform up to 5 boosting rounds"):
reg = lgb.LGBMRegressor(**base_kwargs, num_iterations=5, n_iter=6).fit(X, y)
assert reg.n_estimators_ == 5
# should not be any other logs (except the warning, intercepted by pytest)
assert_silent(capsys)
# warning: 2 aliases (different values... first one in the order from Config::parameter2aliases() wins)
with pytest.warns(UserWarning, match="LightGBM will perform up to 4 boosting rounds"):
reg = lgb.LGBMRegressor(**base_kwargs, n_iter=4, max_iter=5).fit(X, y)
assert reg.n_estimators_ == 4
# should not be any other logs (except the warning, intercepted by pytest)
assert_silent(capsys)
@pytest.mark.parametrize("estimator_class", estimator_classes)
def test_cannot_access_feature_names_before_fitting(estimator_class):
model = estimator_class()
with pytest.raises(lgb.compat.LGBMNotFittedError): # noqa: PT011
model.feature_name_
with pytest.raises(lgb.compat.LGBMNotFittedError): # noqa: PT011
model.feature_names_in_
with pytest.raises(lgb.compat.LGBMNotFittedError): # noqa: PT011
model.n_features_
with pytest.raises(lgb.compat.LGBMNotFittedError): # noqa: PT011
model.n_features_in_
@pytest.mark.parametrize(
"predict_X_type",
[
pytest.param("numpy", id="predict=numpy"),
pytest.param("pd_DataFrame", id="predict=pd_DataFrame"),
pytest.param("pa_Table", id="predict=pa_Table"),
pytest.param("pl_DataFrame", id="predict=pl_DataFrame"),
],
)
@pytest.mark.parametrize(
"fit_X_type",
[
pytest.param("numpy", id="fit=numpy"),
pytest.param("pd_DataFrame", id="fit=pd_DataFrame"),
pytest.param("pa_Table", id="fit=pa_Table"),
pytest.param("pl_DataFrame", id="fit=pl_DataFrame"),
],
)
@pytest.mark.filterwarnings("error:.*feature name.*:UserWarning:sklearn")
def test_feature_names_in_and_predict_warning(
predict_X_type,
fit_X_type,
):
"""Test feature_names_in_ behavior and predict()-time feature name warnings.
Should cover all combinations of fit X type, feature_name argument, and predict X type.
Regression test for https://github.com/lightgbm-org/LightGBM/issues/6798.
"""
if fit_X_type.startswith("pa_") or predict_X_type.startswith("pa_"):
pa = pytest.importorskip("pyarrow")
pd = pytest.importorskip("pandas")
if fit_X_type.startswith("pd_") or predict_X_type.startswith("pd_"):
pd = pytest.importorskip("pandas")
if fit_X_type.startswith("pl_") or predict_X_type.startswith("pl_"):
pl = pytest.importorskip("polars")
X_np = np.array([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0], [7.0, 8.0]])
y = np.array([0, 1, 0, 1])
n_features = X_np.shape[1]
col_names = ["feat_0", "feat_1"]
default_names = ["Column_0", "Column_1"]
if fit_X_type == "numpy":
X_fit = X_np
elif fit_X_type == "pd_DataFrame":
X_fit = pd.DataFrame(X_np, columns=col_names)
elif fit_X_type == "pl_DataFrame":
X_fit = pl.DataFrame(X_np, schema=col_names)
else:
X_fit = pa.Table.from_pandas(pd.DataFrame(X_np, columns=col_names))
if predict_X_type == "numpy":
X_predict = X_np[:2]
elif predict_X_type == "pd_DataFrame":
X_predict = pd.DataFrame(X_np[:2], columns=col_names)
elif predict_X_type == "pl_DataFrame":
X_predict = pl.DataFrame(X_np[:2], schema=col_names)
else:
X_predict = pa.Table.from_pandas(pd.DataFrame(X_np[:2], columns=col_names))
# arguments to warnings.filterwarnings() that match scikit-learn's warning
warning_kwargs = {
"category": UserWarning,
"module": "sklearn",
"message": ".*feature names.*",
}
# input types where LightGBM supports 'feature_name="auto"'
types_with_feat_names = {"pa_Table", "pd_DataFrame", "pl_DataFrame"}
# case 1: no 'feature_names' passed to fit() and "feature_name='auto'" should have identical behavior
for fit_kwargs in ({}, {"feature_name": "auto"}):
model = lgb.LGBMClassifier(n_estimators=2, num_leaves=3).fit(X_fit, y, **fit_kwargs)
# n_features_in_: always set after fit
assert model.n_features_in_ == n_features
# feature_name_: always accessible, reflects actual names used internally
# feature_names_in_: absent when no named features, present otherwise
if fit_X_type in types_with_feat_names:
np_assert_array_equal(model.feature_names_in_, np.array(col_names), strict=True)
assert model.feature_name_ == col_names
else:
assert model.feature_name_ == default_names
with pytest.raises(AttributeError, match="The training data did not have feature names"):
model.feature_names_in_
# predict() should not raise a warning if the input did not have feature names
if SKLEARN_VERSION_GTE_1_6:
# fmt:off
if (
fit_X_type in types_with_feat_names
and
predict_X_type not in types_with_feat_names
):
# fmt:on
# warning may be raised (and was from at least scikit-learn 1.6)
with warnings.catch_warnings():
warnings.filterwarnings("ignore", **warning_kwargs)
model.predict(X_predict)
else:
# expect no warning
with warnings.catch_warnings():
warnings.filterwarnings("error", **warning_kwargs)
model.predict(X_predict)
# case 2: 'feature_names=custom_names' (different from column names) passed to fit()
custom_names = ["custom_0", "custom_1"]
model = lgb.LGBMClassifier(n_estimators=2, num_leaves=3).fit(X_fit, y, feature_name=custom_names)
# feature names from keyword arg should be used, not any from the input data
np_assert_array_equal(model.feature_names_in_, np.array(custom_names), strict=True)
assert model.feature_name_ == custom_names
np_assert_array_equal(model.feature_names_in_, np.array(custom_names), strict=True)
assert model.n_features_in_ == n_features
# predict() should not raise a warning if input has feature names
if SKLEARN_VERSION_GTE_1_6:
if predict_X_type in types_with_feat_names:
# expect no warning
with warnings.catch_warnings():
warnings.filterwarnings("error", **warning_kwargs)
model.predict(X_predict)
else:
# warning may be raised (and was from at least scikit-learn 1.6)
with warnings.catch_warnings():
warnings.filterwarnings("ignore", **warning_kwargs)
model.predict(X_predict)
# Starting with scikit-learn 1.6 (https://github.com/scikit-learn/scikit-learn/pull/30149),
# the only API for marking estimator tests as expected to fail is to pass a keyword argument
# to parametrize_with_checks(). That function didn't accept additional arguments in earlier
# versions.
#
# This block defines a patched version of parametrize_with_checks() so lightgbm's tests
# can be compatible with scikit-learn <1.6 and >=1.6.
#
# This should be removed once minimum supported scikit-learn version is at least 1.6.
if SKLEARN_VERSION_GTE_1_6:
parametrize_with_checks = sklearn_parametrize_with_checks
else:
def parametrize_with_checks(estimator, *args, **kwargs):
return sklearn_parametrize_with_checks(estimator)
def _get_expected_failed_tests(estimator):
return estimator._more_tags()["_xfail_checks"]
@parametrize_with_checks(
[ExtendedLGBMClassifier(), ExtendedLGBMRegressor(), lgb.LGBMClassifier(), lgb.LGBMRegressor()],
expected_failed_checks=_get_expected_failed_tests,
)
def test_sklearn_integration(estimator, check):
estimator.set_params(min_child_samples=1, min_data_in_bin=1)
check(estimator)
@pytest.mark.parametrize("estimator_class", estimator_classes)
def test_sklearn_tags_should_correctly_reflect_lightgbm_specific_values(estimator_class):
est = estimator_class()
more_tags = est._more_tags()
err_msg = "List of supported X_types has changed. Update LGBMModel.__sklearn_tags__() to match."
assert more_tags["X_types"] == ["2darray", "sparse", "1dlabels"], err_msg
# the try-except part of this should be removed once lightgbm's
# minimum supported scikit-learn version is at least 1.6
try:
sklearn_tags = est.__sklearn_tags__()
except AttributeError:
# only the exact error we expected to be raised should be raised
with pytest.raises(AttributeError, match=r"__sklearn_tags__.* should not be called"):
est.__sklearn_tags__()
else:
# if no AttributeError was thrown, we must be using scikit-learn>=1.6,
# and so the actual effects of __sklearn_tags__() should be tested
assert sklearn_tags.input_tags.allow_nan is True
assert sklearn_tags.input_tags.sparse is True
assert sklearn_tags.target_tags.one_d_labels is True
if estimator_class is lgb.LGBMClassifier:
assert sklearn_tags.estimator_type == "classifier"
assert sklearn_tags.classifier_tags.multi_class is True
assert sklearn_tags.classifier_tags.multi_label is False
elif estimator_class is lgb.LGBMRegressor:
assert sklearn_tags.estimator_type == "regressor"
@pytest.mark.parametrize("task", all_tasks)
def test_training_succeeds_when_data_is_dataframe_and_label_is_column_array(task):
pd = pytest.importorskip("pandas")
X, y, g = _create_data(task)
X = pd.DataFrame(X)
y_col_array = y.reshape(-1, 1)
params = {"n_estimators": 1, "num_leaves": 3, "random_state": 0}
model_factory = task_to_model_factory[task]
if task == "ranking":
model_1d = model_factory(**params).fit(X, y, group=g)
with pytest.warns(UserWarning, match="column-vector"):
model_2d = model_factory(**params).fit(X, y_col_array, group=g)
else:
model_1d = model_factory(**params).fit(X, y)
with pytest.warns(UserWarning, match="column-vector"):
model_2d = model_factory(**params).fit(X, y_col_array)
preds_1d = model_1d.predict(X)
preds_2d = model_2d.predict(X)
np_assert_array_equal(preds_1d, preds_2d, strict=True)
@pytest.mark.parametrize("use_weight", [True, False])
def test_multiclass_custom_objective(use_weight):
centers = [[-4, -4], [4, 4], [-4, 4]]
X, y = make_blobs(n_samples=1_000, centers=centers, random_state=42)
weight = np.full_like(y, 2) if use_weight else None
params = {"n_estimators": 10, "num_leaves": 7}
builtin_obj_model = lgb.LGBMClassifier(**params)
builtin_obj_model.fit(X, y, sample_weight=weight)
builtin_obj_preds = builtin_obj_model.predict_proba(X)
custom_obj_model = lgb.LGBMClassifier(objective=sklearn_multiclass_custom_objective, **params)
custom_obj_model.fit(X, y, sample_weight=weight)
custom_obj_preds = softmax(custom_obj_model.predict(X, raw_score=True))
np.testing.assert_allclose(builtin_obj_preds, custom_obj_preds, rtol=0.01)
assert not callable(builtin_obj_model.objective_)
assert callable(custom_obj_model.objective_)
@pytest.mark.parametrize("use_weight", [True, False])
def test_multiclass_custom_eval(use_weight):
def custom_eval(y_true, y_pred, weight):
loss = log_loss(y_true, y_pred, sample_weight=weight)
return "custom_logloss", loss, False
centers = [[-4, -4], [4, 4], [-4, 4]]
X, y = make_blobs(n_samples=1_000, centers=centers, random_state=42)
train_test_split_func = partial(train_test_split, test_size=0.2, random_state=0)
X_train, X_valid, y_train, y_valid = train_test_split_func(X, y)
if use_weight:
weight = np.full_like(y, 2)
weight_train, weight_valid = train_test_split_func(weight)
else:
weight_train = None
weight_valid = None
params = {"objective": "multiclass", "num_class": 3, "num_leaves": 7}
model = lgb.LGBMClassifier(**params)
model.fit(
X_train,
y_train,
sample_weight=weight_train,
eval_set=[(X_train, y_train), (X_valid, y_valid)],
eval_names=["train", "valid"],
eval_sample_weight=[weight_train, weight_valid],
eval_metric=custom_eval,
)
eval_result = model.evals_result_
train_ds = (X_train, y_train, weight_train)
valid_ds = (X_valid, y_valid, weight_valid)
for key, (X, y_true, weight) in zip(["train", "valid"], [train_ds, valid_ds], strict=True):
np.testing.assert_allclose(eval_result[key]["multi_logloss"], eval_result[key]["custom_logloss"])
y_pred = model.predict_proba(X)
_, metric_value, _ = custom_eval(y_true, y_pred, weight)
np.testing.assert_allclose(metric_value, eval_result[key]["custom_logloss"][-1])
def test_negative_n_jobs(tmp_path):
n_threads = joblib.cpu_count()
if n_threads <= 1:
return None
# 'val_minus_two' here is the expected number of threads for n_jobs=-2
val_minus_two = n_threads - 1
X, y = load_breast_cancer(return_X_y=True)
# Note: according to joblib's formula, a value of n_jobs=-2 means
# "use all but one thread" (formula: n_cpus + 1 + n_jobs)
gbm = lgb.LGBMClassifier(n_estimators=2, verbose=-1, n_jobs=-2).fit(X, y)
gbm.booster_.save_model(tmp_path / "model.txt")
with open(tmp_path / "model.txt", "r") as f:
model_txt = f.read()
assert bool(re.search(rf"\[num_threads: {val_minus_two}\]", model_txt))
def test_default_n_jobs(tmp_path):
n_cores = joblib.cpu_count(only_physical_cores=True)
X, y = load_breast_cancer(return_X_y=True)
gbm = lgb.LGBMClassifier(n_estimators=2, verbose=-1, n_jobs=None).fit(X, y)
gbm.booster_.save_model(tmp_path / "model.txt")
with open(tmp_path / "model.txt", "r") as f:
model_txt = f.read()
assert bool(re.search(rf"\[num_threads: {n_cores}\]", model_txt))
@pytest.mark.skipif(not PANDAS_INSTALLED, reason="pandas is not installed")
@pytest.mark.parametrize("task", all_tasks)
def test_validate_features(task):
X, y, g = _create_data(task, n_features=4)
features = ["x1", "x2", "x3", "x4"]
df = pd_DataFrame(X, columns=features)
model = task_to_model_factory[task](n_estimators=10, num_leaves=15, verbose=-1)
if task == "ranking":
model.fit(df, y, group=g)
else:
model.fit(df, y)
assert model.feature_name_ == features
# try to predict with a different feature
df2 = df.rename(columns={"x2": "z"})
with pytest.raises(lgb.basic.LightGBMError, match="Expected 'x2' at position 1 but found 'z'"):
model.predict(df2, validate_features=True)
# check that disabling the check doesn't raise the error
model.predict(df2, validate_features=False)
# LightGBM's 'predict_disable_shape_check' mechanism is intentionally not respected by
# its scikit-learn estimators, for consistency with scikit-learn's own behavior.
@pytest.mark.parametrize("task", all_tasks)
@pytest.mark.parametrize("predict_disable_shape_check", [True, False])
def test_predict_rejects_inputs_with_incorrect_number_of_features(predict_disable_shape_check, task):
X, y, g = _create_data(task, n_features=4)
model_factory = task_to_model_factory[task]
fit_kwargs = {"X": X[:, :-1], "y": y}
if task == "ranking":
estimator_name = "LGBMRanker"
fit_kwargs.update({"group": g})
elif task == "regression":
estimator_name = "LGBMRegressor"
else:
estimator_name = "LGBMClassifier"
# train on the first 3 features
model = model_factory(n_estimators=5, num_leaves=7, verbose=-1).fit(**fit_kwargs)
# more cols in X than features: error
err_msg = f"X has 4 features, but {estimator_name} is expecting 3 features as input"
with pytest.raises(ValueError, match=err_msg):
model.predict(X, predict_disable_shape_check=predict_disable_shape_check)
if estimator_name == "LGBMClassifier":
with pytest.raises(ValueError, match=err_msg):
model.predict_proba(X, predict_disable_shape_check=predict_disable_shape_check)
# fewer cols in X than features: error
err_msg = f"X has 2 features, but {estimator_name} is expecting 3 features as input"
with pytest.raises(ValueError, match=err_msg):
model.predict(X[:, :-2], predict_disable_shape_check=predict_disable_shape_check)
if estimator_name == "LGBMClassifier":
with pytest.raises(ValueError, match=err_msg):
model.predict_proba(X[:, :-2], predict_disable_shape_check=predict_disable_shape_check)
# same number of columns in both: no error
preds = model.predict(X[:, :-1], predict_disable_shape_check=predict_disable_shape_check)
assert preds.shape == y.shape
if estimator_name == "LGBMClassifier":
preds = model.predict_proba(X[:, :-1], predict_disable_shape_check=predict_disable_shape_check)
assert preds.shape[0] == y.shape[0]
def _run_minimal_test(*, X_type, y_type, g_type, task, rng):
if any(t.startswith("pa_") for t in [X_type, y_type, g_type]):
pa = pytest.importorskip("pyarrow")
if any(t.startswith("pl_") for t in [X_type, y_type, g_type]):
pl = pytest.importorskip("polars")
X, y, g = _create_data(task, n_samples=2_000)
weights = np.abs(rng.standard_normal(size=(y.shape[0],)))
if task in {"binary-classification", "regression", "ranking"}:
init_score = np.full_like(y, np.mean(y))
elif task == "multiclass-classification":
init_score = np.outer(y, np.array([0.1, 0.2, 0.7]))
else:
raise ValueError(f"Unrecognized task '{task}'")
X_valid = X * 2
if X_type == "list2d":
X = X.tolist()
elif X_type == "scipy_csc":
X = scipy.sparse.csc_matrix(X)
elif X_type == "scipy_csr":
X = scipy.sparse.csr_matrix(X)
elif X_type == "pd_DataFrame":
X = pd_DataFrame(X)
elif X_type == "pa_Table":
X = pa.Table.from_pandas(pd_DataFrame(X))
elif X_type == "pl_DataFrame":
X = pl.DataFrame(X)
elif X_type != "numpy":
raise ValueError(f"Unrecognized X_type: '{X_type}'")
# make weights and init_score same types as y, just to avoid
# a huge number of combinations and therefore test cases
if y_type == "list1d":
y = y.tolist()
weights = weights.tolist()
init_score = init_score.tolist()
elif y_type == "pd_DataFrame":
y = pd_DataFrame(y)
weights = pd_Series(weights)
if task == "multiclass-classification":
init_score = pd_DataFrame(init_score)
else:
init_score = pd_Series(init_score)
elif y_type == "pd_Series":
y = pd_Series(y)
weights = pd_Series(weights)
if task == "multiclass-classification":
init_score = pd_DataFrame(init_score)
else:
init_score = pd_Series(init_score)
elif y_type == "pa_ChunkedArray":
y = pa.chunked_array([y])
weights = pa.chunked_array([weights])
if task == "multiclass-classification":
init_score = pa.Table.from_pandas(pd_DataFrame(init_score))
else:
init_score = pa.chunked_array([init_score])
elif y_type == "pl_Series":
y = pl.Series(y)
weights = pl.Series(weights)
if task == "multiclass-classification":
init_score = pl.DataFrame(init_score)
else:
init_score = pl.Series(init_score)
elif y_type != "numpy":
raise ValueError(f"Unrecognized y_type: '{y_type}'")
if g_type == "list1d_float":
g = g.astype("float").tolist()
elif g_type == "list1d_int":
g = g.astype("int").tolist()
elif g_type == "pd_Series":
g = pd_Series(g)
elif g_type == "pa_ChunkedArray":
g = pa.chunked_array([g])
elif g_type == "pl_Series":
g = pl.Series(g)
elif g_type != "numpy":
raise ValueError(f"Unrecognized g_type: '{g_type}'")
model = task_to_model_factory[task](n_estimators=10, verbose=-1)
params_fit = {
"X": X,
"y": y,
"sample_weight": weights,
"init_score": init_score,
"eval_set": [(X_valid, y)],
"eval_sample_weight": [weights],
"eval_init_score": [init_score],
}
if task == "ranking":
params_fit["group"] = g
params_fit["eval_group"] = [g]
model.fit(**params_fit)
# --- prediction accuracy --#
preds = model.predict(X)
if task == "binary-classification":
assert accuracy_score(y, preds) >= 0.99
elif task == "multiclass-classification":
assert accuracy_score(y, preds) >= 0.99
elif task == "regression":
assert r2_score(y, preds) > 0.86
elif task == "ranking":
assert spearmanr(preds, y).correlation >= 0.99
else:
raise ValueError(f"Unrecognized task: '{task}'")
# --- prediction dtypes ---#
# default predictions:
#
# * classification: int32 or int64
# * ranking: float64
# * regression: float64
#
if task.endswith("classification"):
# preds go through LabelEncoder.inverse_transform() and have the same
# dtype as model.classes_ (expected to be an integer type, but exact size
# varies across numpy versions and operating systems)
assert preds.dtype == model.classes_.dtype
assert preds.dtype in (np.int32, np.int64)
else:
assert preds.dtype == np.float64
# raw predictions: always float64
preds_raw = model.predict(X, raw_score=True)
assert preds_raw.dtype == np.float64
# pred_contrib: always float64
if X_type.startswith("scipy"):
assert all(arr.dtype == np.float64 for arr in model.predict(X, pred_contrib=True))
else:
preds_contrib = model.predict(X, pred_contrib=True)
assert preds_contrib.dtype == np.float64
# pred_leavs: always int32
preds_leaves = model.predict(X, pred_leaf=True)
assert preds_leaves.dtype == np.int32
@pytest.mark.parametrize("X_type", all_x_types)
@pytest.mark.parametrize("y_type", all_y_types)
@pytest.mark.parametrize("task", [t for t in all_tasks if t != "ranking"])
def test_classification_and_regression_minimally_work_with_all_accepted_data_types(
X_type,
y_type,
task,
rng,
):
if any(t.startswith("pd_") for t in [X_type, y_type]) and not PANDAS_INSTALLED:
pytest.skip("pandas is not installed")
_run_minimal_test(X_type=X_type, y_type=y_type, g_type="numpy", task=task, rng=rng)
@pytest.mark.parametrize("X_type", all_x_types)
@pytest.mark.parametrize("y_type", all_y_types)
@pytest.mark.parametrize("g_type", all_group_types)
def test_ranking_minimally_works_with_all_accepted_data_types(
X_type,
y_type,
g_type,
rng,
):
if any(t.startswith("pd_") for t in [X_type, y_type, g_type]) and not PANDAS_INSTALLED:
pytest.skip("pandas is not installed")
_run_minimal_test(X_type=X_type, y_type=y_type, g_type=g_type, task="ranking", rng=rng)
def test_classifier_fit_detects_classes_every_time():
rng = np.random.default_rng(seed=123)
nrows = 1000
ncols = 20
X = rng.standard_normal(size=(nrows, ncols))
y_bin = (rng.random(size=nrows) <= 0.3).astype(np.float64)
y_multi = rng.integers(4, size=nrows)
model = lgb.LGBMClassifier(verbose=-1)
for _ in range(2):
model.fit(X, y_multi)
assert model.objective_ == "multiclass"
model.fit(X, y_bin)
assert model.objective_ == "binary"
def test_eval_set_deprecation():
"""Test use of eval_set raises deprecation warning."""
X, y = make_synthetic_regression(n_samples=10)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.5, random_state=42)
gbm = lgb.LGBMRegressor()
msg = "The argument 'eval_set' is deprecated.*"
with pytest.warns(LGBMDeprecationWarning, match=msg):
gbm.fit(X_train, y_train, eval_set=[(X_test, y_test)])
def test_eval_set_raises():
"""Test that eval_set and eval_X raise errors where appropriate."""
X, y = make_synthetic_regression(n_samples=10)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.5, random_state=42)
gbm = lgb.LGBMRegressor()
msg = "Specify either 'eval_set' or 'eval_X'.*"
with pytest.raises(ValueError, match=msg):
gbm.fit(X_train, y_train, eval_set=[(X_test, y_test)], eval_X=X_test)
with pytest.raises(ValueError, match=msg):
gbm.fit(X_train, y_train, eval_set=[(X_test, y_test)], eval_y=y_test)
msg = "You must specify eval_X and eval_y, not just one of them."
with pytest.raises(ValueError, match=msg):
gbm.fit(X_train, y_train, eval_X=X_test)
with pytest.raises(ValueError, match=msg):
gbm.fit(X_train, y_train, eval_y=y_test)
msg = "If eval_X is a tuple, y_val must be a tuple of same length, and vice versa."
with pytest.raises(ValueError, match=msg):
gbm.fit(X_train, y_train, eval_X=(X_test, X_test), eval_y=y_test)
with pytest.raises(ValueError, match=msg):
gbm.fit(X_train, y_train, eval_X=X_test, eval_y=(y_test, y_test))
with pytest.raises(ValueError, match=msg):
gbm.fit(X_train, y_train, eval_X=(X_test,) * 3, eval_y=(y_test,) * 2)
def test_ranker_eval_set_raises():
"""Test that LGBMRanker raises expected errors from validating eval_group."""
X, y, g = _create_data(task="ranking", n_samples=1_000)
X_test, y_test, g_test = _create_data(task="ranking", n_samples=100)
gbm = lgb.LGBMRanker()
msg = "eval_group cannot be None if any of eval_set, eval_X, or eval_y are provided"
with pytest.raises(ValueError, match=msg):
gbm.fit(X, y, group=g, eval_set=[(X_test, y_test)])
with pytest.raises(ValueError, match=msg):
gbm.fit(X, y, group=g, eval_X=(X_test,))
with pytest.raises(ValueError, match=msg):
gbm.fit(X, y, group=g, eval_y=(y_test,))
with pytest.raises(ValueError, match=msg):
gbm.fit(X, y, group=g, eval_X=(X_test,), eval_y=(y_test,))
msg = re.escape("Length of eval_group (1) not equal to length of eval_set (2)")
with pytest.raises(ValueError, match=msg):
gbm.fit(X, y, group=g, eval_X=(X_test, X_test), eval_y=(y_test, y_test), eval_group=[g_test])
with pytest.raises(ValueError, match=msg):
gbm.fit(X, y, group=g, eval_set=[(X_test, y_test), (X_test, y_test)], eval_group=[g_test])
def test_eval_X_eval_y_eval_set_equivalence():
"""Test that eval_X and eval_y are equivalent to eval_set."""
X, y = make_synthetic_regression()
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=42)
params = {
"deterministic": True,
"force_row_wise": True,
"n_jobs": 1,
"seed": 708,
}
cbs = [lgb.early_stopping(2)]
gbm1 = lgb.LGBMRegressor(**params)
with pytest.warns(LGBMDeprecationWarning, match="The argument 'eval_set' is deprecated.*"):
gbm1.fit(X_train, y_train, eval_set=[(X_test, y_test)], callbacks=cbs)
gbm2 = lgb.LGBMRegressor(**params)
gbm2.fit(X_train, y_train, eval_X=X_test, eval_y=y_test, callbacks=cbs)
np.testing.assert_allclose(gbm1.predict(X), gbm2.predict(X))
assert gbm1.evals_result_["valid_0"]["l2"][0] == pytest.approx(gbm2.evals_result_["valid_0"]["l2"][0])
# 2 evaluation sets
n = X_test.shape[0]
X_test1, X_test2 = X_test[: n // 2], X_test[n // 2 :]
y_test1, y_test2 = y_test[: n // 2], y_test[n // 2 :]
gbm1 = lgb.LGBMRegressor(**params)
with pytest.warns(LGBMDeprecationWarning, match="The argument 'eval_set' is deprecated.*"):
gbm1.fit(X_train, y_train, eval_set=[(X_test1, y_test1), (X_test2, y_test2)], callbacks=cbs)
gbm2 = lgb.LGBMRegressor(**params)
gbm2.fit(X_train, y_train, eval_X=(X_test1, X_test2), eval_y=(y_test1, y_test2), callbacks=cbs)
np.testing.assert_allclose(gbm1.predict(X), gbm2.predict(X))
assert set(gbm2.evals_result_.keys()) == {"valid_0", "valid_1"}, (
f"expected 2 validation sets, 'valid_0' and 'valid_1', in evals_result_, got {gbm2.evals_result_.keys()}"
)
assert gbm1.evals_result_["valid_0"]["l2"][0] == pytest.approx(gbm2.evals_result_["valid_0"]["l2"][0])
assert gbm1.evals_result_["valid_1"]["l2"][0] == pytest.approx(gbm2.evals_result_["valid_1"]["l2"][0])
assert gbm2.evals_result_["valid_0"]["l2"] != gbm2.evals_result_["valid_1"]["l2"], (
"Evaluation results for the 2 validation sets are not different. This might mean they weren't both used."
)