test_sklearn.py 86.7 KB
Newer Older
wxchan's avatar
wxchan committed
1
# coding: utf-8
2
import inspect
3
import itertools
4
import math
5
import re
6
from functools import partial
7
from os import getenv
8
from pathlib import Path
wxchan's avatar
wxchan committed
9

10
import joblib
wxchan's avatar
wxchan committed
11
import numpy as np
12
import pytest
13
14
import scipy.sparse
from scipy.stats import spearmanr
wxchan's avatar
wxchan committed
15
from sklearn.base import clone
16
from sklearn.datasets import load_svmlight_file, make_blobs, make_multilabel_classification
17
from sklearn.ensemble import StackingClassifier, StackingRegressor
18
from sklearn.metrics import accuracy_score, log_loss, mean_squared_error, r2_score
19
from sklearn.model_selection import GridSearchCV, RandomizedSearchCV, train_test_split
20
from sklearn.multioutput import ClassifierChain, MultiOutputClassifier, MultiOutputRegressor, RegressorChain
21
from sklearn.utils.estimator_checks import parametrize_with_checks as sklearn_parametrize_with_checks
22
from sklearn.utils.validation import check_is_fitted
wxchan's avatar
wxchan committed
23

24
import lightgbm as lgb
25
from lightgbm.compat import (
26
    DASK_INSTALLED,
27
    PANDAS_INSTALLED,
28
    PYARROW_INSTALLED,
29
    _sklearn_version,
30
31
32
    pa_array,
    pa_chunked_array,
    pa_Table,
33
34
35
    pd_DataFrame,
    pd_Series,
)
36

37
from .utils import (
38
    assert_silent,
39
40
41
42
43
44
    load_breast_cancer,
    load_digits,
    load_iris,
    load_linnerud,
    make_ranking,
    make_synthetic_regression,
45
    np_assert_array_equal,
46
47
48
    sklearn_multiclass_custom_objective,
    softmax,
)
49

50
51
52
SKLEARN_MAJOR, SKLEARN_MINOR, *_ = _sklearn_version.split(".")
SKLEARN_VERSION_GTE_1_6 = (int(SKLEARN_MAJOR), int(SKLEARN_MINOR)) >= (1, 6)

53
decreasing_generator = itertools.count(0, -1)
54
estimator_classes = (lgb.LGBMModel, lgb.LGBMClassifier, lgb.LGBMRegressor, lgb.LGBMRanker)
55
task_to_model_factory = {
56
57
58
59
    "ranking": lgb.LGBMRanker,
    "binary-classification": lgb.LGBMClassifier,
    "multiclass-classification": lgb.LGBMClassifier,
    "regression": lgb.LGBMRegressor,
60
}
61
all_tasks = tuple(task_to_model_factory.keys())
62
63
64
all_x_types = ("list2d", "numpy", "pd_DataFrame", "pa_Table", "scipy_csc", "scipy_csr")
all_y_types = ("list1d", "numpy", "pd_Series", "pd_DataFrame", "pa_Array", "pa_ChunkedArray")
all_group_types = ("list1d_float", "list1d_int", "numpy", "pd_Series", "pa_Array", "pa_ChunkedArray")
65
66


67
def _create_data(task, n_samples=100, n_features=4):
68
    if task == "ranking":
69
        X, y, g = make_ranking(n_features=4, n_samples=n_samples)
70
        g = np.bincount(g)
71
72
    elif task.endswith("classification"):
        if task == "binary-classification":
73
            centers = 2
74
        elif task == "multiclass-classification":
75
76
            centers = 3
        else:
77
            raise ValueError(f"Unknown classification task '{task}'")
78
        X, y = make_blobs(n_samples=n_samples, n_features=n_features, centers=centers, random_state=42)
79
        g = None
80
    elif task == "regression":
81
        X, y = make_synthetic_regression(n_samples=n_samples, n_features=n_features)
82
83
        g = None
    return X, y, g
wxchan's avatar
wxchan committed
84

wxchan's avatar
wxchan committed
85

86
87
88
89
90
class UnpicklableCallback:
    def __reduce__(self):
        raise Exception("This class in not picklable")

    def __call__(self, env):
91
        env.model.attr_set_inside_callback = env.iteration * 10
92
93


94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
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)


118
def custom_asymmetric_obj(y_true, y_pred):
119
    residual = (y_true - y_pred).astype(np.float64)
120
121
122
123
124
    grad = np.where(residual < 0, -2 * 10.0 * residual, -2 * residual)
    hess = np.where(residual < 0, 2 * 10.0, 2.0)
    return grad, hess


125
def objective_ls(y_true, y_pred):
126
    grad = y_pred - y_true
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
    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):
143
    return "error", 0, False
144
145
146


def decreasing_metric(y_true, y_pred):
147
    return ("decreasing_metric", next(decreasing_generator), False)
148
149


150
def mse(y_true, y_pred):
151
    return "custom MSE", mean_squared_error(y_true, y_pred), False
152
153


154
155
156
157
158
159
160
161
162
163
164
165
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)])


166
167
168
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)
169
    gbm = lgb.LGBMClassifier(n_estimators=50, verbose=-1)
170
    gbm.fit(X_train, y_train, eval_set=[(X_test, y_test)], callbacks=[lgb.early_stopping(5)])
171
172
    ret = log_loss(y_test, gbm.predict_proba(X_test))
    assert ret < 0.12
173
    assert gbm.evals_result_["valid_0"]["binary_logloss"][gbm.best_iteration_ - 1] == pytest.approx(ret)
174
175
176


def test_regression():
177
    X, y = make_synthetic_regression()
178
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, random_state=42)
179
    gbm = lgb.LGBMRegressor(n_estimators=50, verbose=-1)
180
    gbm.fit(X_train, y_train, eval_set=[(X_test, y_test)], callbacks=[lgb.early_stopping(5)])
181
    ret = mean_squared_error(y_test, gbm.predict(X_test))
182
    assert ret < 174
183
    assert gbm.evals_result_["valid_0"]["l2"][gbm.best_iteration_ - 1] == pytest.approx(ret)
184
185


186
187
188
@pytest.mark.skipif(
    getenv("TASK", "") == "cuda", reason="Skip due to differences in implementation details of CUDA version"
)
189
190
191
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)
192
    gbm = lgb.LGBMClassifier(n_estimators=50, verbose=-1)
193
    gbm.fit(X_train, y_train, eval_set=[(X_test, y_test)], callbacks=[lgb.early_stopping(5)])
194
195
196
197
    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
198
    assert gbm.evals_result_["valid_0"]["multi_logloss"][gbm.best_iteration_ - 1] == pytest.approx(ret)
199
200


201
202
203
@pytest.mark.skipif(
    getenv("TASK", "") == "cuda", reason="Skip due to differences in implementation details of CUDA version"
)
204
def test_lambdarank():
205
206
207
208
209
    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"))
210
    gbm = lgb.LGBMRanker(n_estimators=50)
211
212
213
214
215
216
217
    gbm.fit(
        X_train,
        y_train,
        group=q_train,
        eval_set=[(X_test, y_test)],
        eval_group=[q_test],
        eval_at=[1, 3],
218
        callbacks=[lgb.early_stopping(10), lgb.reset_parameter(learning_rate=lambda x: max(0.01, 0.1 - 0.01 * x))],
219
    )
220
    assert gbm.best_iteration_ <= 24
221
222
    assert gbm.best_score_["valid_0"]["ndcg@1"] > 0.5674
    assert gbm.best_score_["valid_0"]["ndcg@3"] > 0.578
223
224
225


def test_xendcg():
226
227
228
229
230
231
    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)
232
233
234
235
236
237
238
    gbm.fit(
        X_train,
        y_train,
        group=q_train,
        eval_set=[(X_test, y_test)],
        eval_group=[q_test],
        eval_at=[1, 3],
239
240
        eval_metric="ndcg",
        callbacks=[lgb.early_stopping(10), lgb.reset_parameter(learning_rate=lambda x: max(0.01, 0.1 - 0.01 * x))],
241
    )
242
    assert gbm.best_iteration_ <= 24
243
244
    assert gbm.best_score_["valid_0"]["ndcg@1"] > 0.6211
    assert gbm.best_score_["valid_0"]["ndcg@3"] > 0.6253
245
246


247
def test_eval_at_aliases():
248
249
250
251
252
253
    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"):
254
255
256
        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])
257
        assert list(gbm.evals_result_["valid_0"].keys()) == ["ndcg@1", "ndcg@2", "ndcg@3", "ndcg@9"]
258
259


260
261
@pytest.mark.parametrize("custom_objective", [True, False])
def test_objective_aliases(custom_objective):
262
    X, y = make_synthetic_regression()
263
264
265
    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
266
        metric_name = "l2"  # default one
267
    else:
268
269
        obj = "mape"
        metric_name = "mape"
270
    evals = []
271
    for alias in lgb.basic._ConfigAliases.get("objective"):
272
        gbm = lgb.LGBMRegressor(n_estimators=5, **{alias: obj})
273
274
275
276
        if alias != "objective":
            with pytest.warns(
                UserWarning, match=f"Found '{alias}' in params. Will use it instead of 'objective' argument"
            ):
277
278
279
                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)])
280
281
        assert list(gbm.evals_result_["valid_0"].keys()) == [metric_name]
        evals.append(gbm.evals_result_["valid_0"][metric_name])
282
283
284
285
286
287
288
289
    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])


290
def test_regression_with_custom_objective():
291
    X, y = make_synthetic_regression()
292
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, random_state=42)
293
    gbm = lgb.LGBMRegressor(n_estimators=50, verbose=-1, objective=objective_ls)
294
    gbm.fit(X_train, y_train, eval_set=[(X_test, y_test)], callbacks=[lgb.early_stopping(5)])
295
    ret = mean_squared_error(y_test, gbm.predict(X_test))
296
    assert ret < 174
297
    assert gbm.evals_result_["valid_0"]["l2"][gbm.best_iteration_ - 1] == pytest.approx(ret)
298
299
300
301
302


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)
303
    gbm = lgb.LGBMClassifier(n_estimators=50, verbose=-1, objective=logregobj)
304
    gbm.fit(X_train, y_train, eval_set=[(X_test, y_test)], callbacks=[lgb.early_stopping(5)])
305
306
307
308
309
310
311
312
313
    # 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():
314
    X, y = make_synthetic_regression()
315
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, random_state=42)
316
    gbm = lgb.LGBMRegressor(boosting_type="dart", n_estimators=50)
317
318
    gbm.fit(X_train, y_train)
    score = gbm.score(X_test, y_test)
319
    assert 0.8 <= score <= 1.0
320
321
322
323
324


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)
325
326
327
328
    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
    )
329
330
331
    clf.fit(X_train, y_train)
    score = clf.score(X_test, y_test)
    assert score >= 0.8
332
    assert score <= 1.0
333
    assert clf.n_features_in_ == 4  # number of input features
334
335
    assert len(clf.named_estimators_["gbm1"].feature_importances_) == 4
    assert clf.named_estimators_["gbm1"].n_features_in_ == clf.named_estimators_["gbm2"].n_features_in_
336
337
    assert clf.final_estimator_.n_features_in_ == 10  # number of concatenated features
    assert len(clf.final_estimator_.feature_importances_) == 10
338
339
    assert all(clf.named_estimators_["gbm1"].classes_ == clf.named_estimators_["gbm2"].classes_)
    assert all(clf.classes_ == clf.named_estimators_["gbm1"].classes_)
340
341
342


def test_stacking_regressor():
343
344
345
    X, y = make_synthetic_regression(n_samples=200)
    n_features = X.shape[1]
    n_input_models = 2
346
    X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)
347
348
    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)
349
350
351
    reg.fit(X_train, y_train)
    score = reg.score(X_test, y_test)
    assert score >= 0.2
352
    assert score <= 1.0
353
    assert reg.n_features_in_ == n_features  # number of input features
354
355
    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_
356
357
    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
358
359
360
361
362


def test_grid_search():
    X, y = load_iris(return_X_y=True)
    y = y.astype(str)  # utilize label encoder at it's max power
363
364
    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)
365
366
    params = {"subsample": 0.8, "subsample_freq": 1}
    grid_params = {"boosting_type": ["rf", "gbdt"], "n_estimators": [4, 6], "reg_alpha": [0.01, 0.005]}
367
    evals_result = {}
368
369
370
    fit_params = {
        "eval_set": [(X_val, y_val)],
        "eval_metric": constant_metric,
371
        "callbacks": [lgb.early_stopping(2), lgb.record_evaluation(evals_result)],
372
    }
373
    grid = GridSearchCV(estimator=lgb.LGBMClassifier(**params), param_grid=grid_params, cv=2)
374
375
    grid.fit(X_train, y_train, **fit_params)
    score = grid.score(X_test, y_test)  # utilizes GridSearchCV default refit=True
376
377
378
379
    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
380
    assert grid.best_estimator_.best_iteration_ == 1
381
382
    assert grid.best_estimator_.best_score_["valid_0"]["multi_logloss"] < 0.25
    assert grid.best_estimator_.best_score_["valid_0"]["error"] == 0
383
    assert score >= 0.2
384
    assert score <= 1.0
385
    assert evals_result == grid.best_estimator_.evals_result_
386
387


388
def test_random_search(rng):
389
390
    X, y = load_iris(return_X_y=True)
    y = y.astype(str)  # utilize label encoder at it's max power
391
392
    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)
393
    n_iter = 3  # Number of samples
394
    params = {"subsample": 0.8, "subsample_freq": 1}
395
    param_dist = {
396
        "boosting_type": ["rf", "gbdt"],
397
398
        "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(),
399
    }
400
401
402
403
    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
    )
404
405
    rand.fit(X_train, y_train, **fit_params)
    score = rand.score(X_test, y_test)  # utilizes RandomizedSearchCV default refit=True
406
407
408
409
410
411
412
    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
413
    assert score >= 0.2
414
    assert score <= 1.0
415
416
417
418


def test_multioutput_classifier():
    n_outputs = 3
419
    X, y = make_multilabel_classification(n_samples=100, n_features=20, n_classes=n_outputs, random_state=0)
420
    y = y.astype(str)  # utilize label encoder at it's max power
421
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, random_state=42)
422
423
424
425
    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
426
    assert score <= 1.0
427
    np_assert_array_equal(np.tile(np.unique(y_train), n_outputs), np.concatenate(clf.classes_), strict=True)
428
429
430
431
432
433
434
    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
435
436
    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)
437
438
439
440
441
    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
442
    assert score <= 120.0
443
444
445
446
447
448
449
    for regressor in reg.estimators_:
        assert isinstance(regressor, lgb.LGBMRegressor)
        assert isinstance(regressor.booster_, lgb.Booster)


def test_classifier_chain():
    n_outputs = 3
450
451
    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)
452
    order = [2, 0, 1]
453
    clf = ClassifierChain(base_estimator=lgb.LGBMClassifier(n_estimators=10), order=order, random_state=42)
454
455
456
    clf.fit(X_train, y_train)
    score = clf.score(X_test, y_test)
    assert score >= 0.2
457
    assert score <= 1.0
458
    np_assert_array_equal(np.tile(np.unique(y_train), n_outputs), np.concatenate(clf.classes_), strict=True)
459
460
461
462
463
464
465
466
    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
467
    X, y = bunch["data"], bunch["target"]
468
469
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, random_state=42)
    order = [2, 0, 1]
470
    reg = RegressorChain(base_estimator=lgb.LGBMRegressor(n_estimators=10), order=order, random_state=42)
471
472
473
474
    reg.fit(X_train, y_train)
    y_pred = reg.predict(X_test)
    _, score, _ = mse(y_test, y_pred)
    assert score >= 0.2
475
    assert score <= 120.0
476
477
478
479
480
481
482
    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():
483
    X, y = make_synthetic_regression()
484
    gbm = lgb.LGBMRegressor(n_estimators=10, verbose=-1)
485
    gbm.fit(X, y)
486
487

    gbm_clone = clone(gbm)
488
489
490
491

    # original estimator is unaffected
    assert gbm.n_estimators == 10
    assert gbm.verbose == -1
492
493
494
    assert isinstance(gbm.booster_, lgb.Booster)
    assert isinstance(gbm.feature_importances_, np.ndarray)

495
496
497
498
499
500
    # 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()

501
    X, y = load_digits(n_class=2, return_X_y=True)
502
    clf = lgb.LGBMClassifier(n_estimators=10, verbose=-1)
503
    clf.fit(X, y)
504
505
506
507
508
509
    assert sorted(clf.classes_) == [0, 1]
    assert clf.n_classes_ == 2
    assert isinstance(clf.booster_, lgb.Booster)
    assert isinstance(clf.feature_importances_, np.ndarray)


510
@pytest.mark.parametrize("estimator", (lgb.LGBMClassifier, lgb.LGBMRegressor, lgb.LGBMRanker))  # noqa: PT007
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
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,
        }

    if DASK_INSTALLED:
        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


697
def test_joblib(tmp_path):
698
    X, y = make_synthetic_regression()
699
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, random_state=42)
700
    gbm = lgb.LGBMRegressor(n_estimators=10, objective=custom_asymmetric_obj, verbose=-1, importance_type="split")
701
702
703
    gbm.fit(
        X_train,
        y_train,
704
        eval_set=[(X_train, y_train), (X_test, y_test)],
705
        eval_metric=mse,
706
        callbacks=[lgb.early_stopping(5), lgb.reset_parameter(learning_rate=list(np.arange(1, 0, -0.1)))],
707
    )
708
709
710
    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)
711
712
    assert isinstance(gbm_pickle.booster_, lgb.Booster)
    assert gbm.get_params() == gbm_pickle.get_params()
713
    np_assert_array_equal(gbm.feature_importances_, gbm_pickle.feature_importances_, strict=True)
714
715
716
717
718
    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]:
719
            np.testing.assert_allclose(gbm.evals_result_[eval_set][metric], gbm_pickle.evals_result_[eval_set][metric])
720
721
722
723
724
    pred_origin = gbm.predict(X_test)
    pred_pickle = gbm_pickle.predict(X_test)
    np.testing.assert_allclose(pred_origin, pred_pickle)


725
726
727
728
def test_non_serializable_objects_in_callbacks(tmp_path):
    unpicklable_callback = UnpicklableCallback()

    with pytest.raises(Exception, match="This class in not picklable"):
729
        joblib.dump(unpicklable_callback, tmp_path / "tmp.joblib")
730

731
    X, y = make_synthetic_regression()
732
733
    gbm = lgb.LGBMRegressor(n_estimators=5)
    gbm.fit(X, y, callbacks=[unpicklable_callback])
734
    assert gbm.booster_.attr_set_inside_callback == 40
735
736


737
738
@pytest.mark.parametrize("rng_constructor", [np.random.RandomState, np.random.default_rng])
def test_random_state_object(rng_constructor):
739
740
    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)
741
742
    state1 = rng_constructor(123)
    state2 = rng_constructor(123)
743
744
745
746
747
748
749
750
751
752
753
    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)
754
    np_assert_array_equal(clf1.feature_importances_, clf2.feature_importances_, strict=True)
755
756
757
758
759
760
761
762
763
    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
764
    with pytest.raises(AssertionError):  # noqa: PT011
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
        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)
781
    clf.set_params(importance_type="split")
782
    importances_split = clf.feature_importances_
783
    clf.set_params(importance_type="gain")
784
785
786
787
788
789
790
    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


791
792
# why fixed seed?
# sometimes there is no difference how cols are treated (cat or not cat)
793
def test_pandas_categorical(rng_fixed_seed, tmp_path):
794
    pd = pytest.importorskip("pandas")
795
796
    X = pd.DataFrame(
        {
797
798
799
800
801
            "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),
802
803
        }
    )  # str and ordered categorical
804
    y = rng_fixed_seed.permutation([0, 1] * 150)
805
806
    X_test = pd.DataFrame(
        {
807
808
809
810
811
            "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),
812
813
        }
    )
814
815
    cat_cols_actual = ["A", "B", "C", "D"]
    cat_cols_to_store = cat_cols_actual + ["E"]
816
817
    X[cat_cols_actual] = X[cat_cols_actual].astype("category")
    X_test[cat_cols_actual] = X_test[cat_cols_actual].astype("category")
818
819
820
821
822
823
    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)
824
    gbm2 = lgb.sklearn.LGBMClassifier(n_estimators=10).fit(X, y, categorical_feature=["A"])
825
    pred2 = gbm2.predict(X_test, raw_score=True)
826
    gbm3 = lgb.sklearn.LGBMClassifier(n_estimators=10).fit(X, y, categorical_feature=["A", "B", "C", "D"])
827
    pred3 = gbm3.predict(X_test, raw_score=True)
828
829
830
    categorical_model_path = tmp_path / "categorical.model"
    gbm3.booster_.save_model(categorical_model_path)
    gbm4 = lgb.Booster(model_file=categorical_model_path)
831
    pred4 = gbm4.predict(X_test)
832
    gbm5 = lgb.sklearn.LGBMClassifier(n_estimators=10).fit(X, y, categorical_feature=["A", "B", "C", "D", "E"])
833
834
835
    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)
836
    with pytest.raises(AssertionError):  # noqa: PT011
837
        np.testing.assert_allclose(pred0, pred1)
838
    with pytest.raises(AssertionError):  # noqa: PT011
839
840
841
842
        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)
843
    with pytest.raises(AssertionError):  # noqa: PT011
844
        np.testing.assert_allclose(pred0, pred5)  # ordered cat features aren't treated as cat features by default
845
    with pytest.raises(AssertionError):  # noqa: PT011
846
847
848
849
850
851
852
853
854
855
        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


856
def test_pandas_sparse(rng):
857
    pd = pytest.importorskip("pandas")
858
859
    X = pd.DataFrame(
        {
860
861
862
            "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)),
863
864
        }
    )
865
    y = pd.Series(pd.arrays.SparseArray(rng.permutation([0, 1] * 150)))
866
867
    X_test = pd.DataFrame(
        {
868
869
870
            "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)),
871
872
        }
    )
873
    for dtype in pd.concat([X.dtypes, X_test.dtypes, pd.Series(y.dtypes)]):
874
        assert isinstance(dtype, pd.SparseDtype)
875
876
    gbm = lgb.sklearn.LGBMClassifier(n_estimators=10).fit(X, y)
    pred_sparse = gbm.predict(X_test, raw_score=True)
877
    if hasattr(X_test, "sparse"):
878
879
880
881
882
883
884
885
886
        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)
887
    X_train, X_test, y_train, _ = train_test_split(iris.data, iris.target, test_size=0.2, random_state=42)
888

889
    gbm = lgb.train({"objective": "multiclass", "num_class": 3, "verbose": -1}, lgb.Dataset(X_train, y_train))
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
    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)
919
    res_sklearn_params = clf.predict_proba(X_test, pred_early_stop=True, pred_early_stop_margin=1.0)
920
    with pytest.raises(AssertionError):  # noqa: PT011
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
        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)
951
    res_sklearn_params = clf.predict_proba(X_test, pred_early_stop=True, pred_early_stop_margin=1.0, start_iteration=10)
952
    with pytest.raises(AssertionError):  # noqa: PT011
953
954
        np.testing.assert_allclose(res_engine, res_sklearn_params)

955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
    # 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)

974

975
976
977
978
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)

979
    predict_params = {"pred_early_stop": True, "pred_early_stop_margin": 1.0}
980

981
    y_preds_no_params = lgb.LGBMClassifier(verbose=-1).fit(X_train, y_train).predict(X_test, raw_score=True)
982

983
984
985
    y_preds_params_in_predict = (
        lgb.LGBMClassifier(verbose=-1).fit(X_train, y_train).predict(X_test, raw_score=True, **predict_params)
    )
986
    with pytest.raises(AssertionError):  # noqa: PT011
987
988
        np.testing.assert_allclose(y_preds_no_params, y_preds_params_in_predict)

989
990
991
992
993
994
    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)
    )
995
996
    np.testing.assert_allclose(y_preds_params_in_predict, y_preds_params_in_set_params_before_fit)

997
998
999
1000
1001
1002
    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)
    )
1003
1004
    np.testing.assert_allclose(y_preds_params_in_predict, y_preds_params_in_set_params_after_fit)

1005
1006
1007
    y_preds_params_in_init = (
        lgb.LGBMClassifier(verbose=-1, **predict_params).fit(X_train, y_train).predict(X_test, raw_score=True)
    )
1008
1009
1010
    np.testing.assert_allclose(y_preds_params_in_predict, y_preds_params_in_init)

    # test that params passed in predict have higher priority
1011
1012
1013
1014
1015
    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)
    )
1016
1017
1018
    np.testing.assert_allclose(y_preds_no_params, y_preds_params_overwritten)


1019
def test_evaluate_train_set():
1020
    X, y = make_synthetic_regression()
1021
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, random_state=42)
1022
    gbm = lgb.LGBMRegressor(n_estimators=10, verbose=-1)
1023
    gbm.fit(X_train, y_train, eval_set=[(X_train, y_train), (X_test, y_test)])
1024
    assert len(gbm.evals_result_) == 2
1025
1026
1027
1028
1029
1030
    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"]
1031
1032
1033


def test_metrics():
1034
1035
    X, y = make_synthetic_regression()
    y = abs(y)
1036
1037
    params = {"n_estimators": 2, "verbose": -1}
    params_fit = {"X": X, "y": y, "eval_set": (X, y)}
1038
1039
1040
1041

    # no custom objective, no custom metric
    # default metric
    gbm = lgb.LGBMRegressor(**params).fit(**params_fit)
1042
1043
    assert len(gbm.evals_result_["training"]) == 1
    assert "l2" in gbm.evals_result_["training"]
1044
1045

    # non-default metric
1046
1047
1048
    gbm = lgb.LGBMRegressor(metric="mape", **params).fit(**params_fit)
    assert len(gbm.evals_result_["training"]) == 1
    assert "mape" in gbm.evals_result_["training"]
1049
1050

    # no metric
1051
    gbm = lgb.LGBMRegressor(metric="None", **params).fit(**params_fit)
1052
    assert gbm.evals_result_ == {}
1053
1054

    # non-default metric in eval_metric
1055
1056
1057
1058
    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"]
1059
1060

    # non-default metric with non-default metric in eval_metric
1061
1062
1063
1064
    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"]
1065
1066

    # non-default metric with multiple metrics in eval_metric
1067
1068
1069
1070
1071
    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"]
1072
1073
1074

    # non-default metric with multiple metrics in eval_metric for LGBMClassifier
    X_classification, y_classification = load_breast_cancer(return_X_y=True)
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
    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"]
1086
1087

    # default metric for non-default objective
1088
1089
1090
    gbm = lgb.LGBMRegressor(objective="regression_l1", **params).fit(**params_fit)
    assert len(gbm.evals_result_["training"]) == 1
    assert "l1" in gbm.evals_result_["training"]
1091
1092

    # non-default metric for non-default objective
1093
1094
1095
    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"]
1096
1097

    # no metric
1098
    gbm = lgb.LGBMRegressor(objective="regression_l1", metric="None", **params).fit(**params_fit)
1099
    assert gbm.evals_result_ == {}
1100
1101

    # non-default metric in eval_metric for non-default objective
1102
1103
1104
1105
    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"]
1106
1107

    # non-default metric with non-default metric in eval_metric for non-default objective
1108
1109
1110
1111
    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"]
1112
1113

    # non-default metric with multiple metrics in eval_metric for non-default objective
1114
1115
1116
1117
1118
1119
1120
    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"]
1121
1122
1123
1124

    # custom objective, no custom metric
    # default regression metric for custom objective
    gbm = lgb.LGBMRegressor(objective=custom_dummy_obj, **params).fit(**params_fit)
1125
1126
    assert len(gbm.evals_result_["training"]) == 1
    assert "l2" in gbm.evals_result_["training"]
1127
1128

    # non-default regression metric for custom objective
1129
1130
1131
    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"]
1132
1133

    # multiple regression metrics for custom objective
1134
1135
1136
1137
    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"]
1138
1139

    # no metric
1140
    gbm = lgb.LGBMRegressor(objective=custom_dummy_obj, metric="None", **params).fit(**params_fit)
1141
    assert gbm.evals_result_ == {}
1142
1143

    # default regression metric with non-default metric in eval_metric for custom objective
1144
1145
1146
1147
    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"]
1148
1149

    # non-default regression metric with metric in eval_metric for custom objective
1150
1151
1152
1153
    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"]
1154
1155

    # multiple regression metrics with metric in eval_metric for custom objective
1156
1157
1158
1159
1160
1161
1162
    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"]
1163
1164

    # multiple regression metrics with multiple metrics in eval_metric for custom objective
1165
1166
1167
1168
1169
1170
1171
1172
    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"]
1173
1174
1175
1176

    # no custom objective, custom metric
    # default metric with custom metric
    gbm = lgb.LGBMRegressor(**params).fit(eval_metric=constant_metric, **params_fit)
1177
1178
1179
    assert len(gbm.evals_result_["training"]) == 2
    assert "l2" in gbm.evals_result_["training"]
    assert "error" in gbm.evals_result_["training"]
1180
1181

    # non-default metric with custom metric
1182
1183
1184
1185
    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"]
1186
1187

    # multiple metrics with custom metric
1188
1189
1190
1191
1192
    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"]
1193
1194

    # custom metric (disable default metric)
1195
1196
1197
    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"]
1198
1199

    # default metric for non-default objective with custom metric
1200
1201
1202
1203
    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"]
1204
1205

    # non-default metric for non-default objective with custom metric
1206
1207
1208
1209
1210
1211
    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"]
1212
1213

    # multiple metrics for non-default objective with custom metric
1214
1215
1216
1217
1218
1219
1220
    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"]
1221
1222

    # custom metric (disable default metric for non-default objective)
1223
1224
1225
1226
1227
    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"]
1228
1229
1230

    # custom objective, custom metric
    # custom metric for custom objective
1231
1232
1233
    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"]
1234
1235

    # non-default regression metric with custom metric for custom objective
1236
1237
1238
1239
1240
1241
    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"]
1242
1243

    # multiple regression metrics with custom metric for custom objective
1244
1245
1246
1247
1248
1249
1250
    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"]
1251
1252

    X, y = load_digits(n_class=3, return_X_y=True)
1253
    params_fit = {"X": X, "y": y, "eval_set": (X, y)}
1254
1255

    # default metric and invalid binary metric is replaced with multiclass alternative
1256
1257
1258
1259
    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"]
1260

1261
    # invalid binary metric is replaced with multiclass alternative
1262
1263
1264
1265
1266
    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"]
1267
1268
1269

    # default metric for non-default multiclass objective
    # and invalid binary metric is replaced with multiclass alternative
1270
1271
1272
1273
1274
    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"]
1275
1276

    X, y = load_digits(n_class=2, return_X_y=True)
1277
    params_fit = {"X": X, "y": y, "eval_set": (X, y)}
1278
1279

    # default metric and invalid multiclass metric is replaced with binary alternative
1280
1281
1282
1283
    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"]
1284
1285

    # invalid multiclass metric is replaced with binary alternative for custom objective
1286
1287
1288
    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"]
1289

1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
    # 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"]

1304
1305
1306
1307

def test_multiple_eval_metrics():
    X, y = load_breast_cancer(return_X_y=True)

1308
1309
    params = {"n_estimators": 2, "verbose": -1, "objective": "binary", "metric": "binary_logloss"}
    params_fit = {"X": X, "y": y, "eval_set": (X, y)}
1310
1311
1312

    # Verify that can receive a list of metrics, only callable
    gbm = lgb.LGBMClassifier(**params).fit(eval_metric=[constant_metric, decreasing_metric], **params_fit)
1313
1314
1315
1316
    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"]
1317
1318

    # Verify that can receive a list of custom and built-in metrics
1319
1320
1321
1322
1323
1324
    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"]
1325
1326
1327

    # Verify that works as expected when eval_metric is empty
    gbm = lgb.LGBMClassifier(**params).fit(eval_metric=[], **params_fit)
1328
1329
    assert len(gbm.evals_result_["training"]) == 1
    assert "binary_logloss" in gbm.evals_result_["training"]
1330
1331

    # Verify that can receive a list of metrics, only built-in
1332
1333
1334
    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"]
1335
1336

    # Verify that eval_metric is robust to receiving a list with None
1337
1338
1339
    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"]
1340
1341


1342
def test_nan_handle(rng):
1343
1344
    nrows = 100
    ncols = 10
1345
1346
    X = rng.standard_normal(size=(nrows, ncols))
    y = rng.standard_normal(size=(nrows,)) + np.full(nrows, 1e30)
1347
    weight = np.zeros(nrows)
1348
1349
    params = {"n_estimators": 20, "verbose": -1}
    params_fit = {"X": X, "y": y, "sample_weight": weight, "eval_set": (X, y), "callbacks": [lgb.early_stopping(5)]}
1350
    gbm = lgb.LGBMRegressor(**params).fit(**params_fit)
1351
    np.testing.assert_allclose(gbm.evals_result_["training"]["l2"], np.nan)
1352
1353


1354
1355
1356
@pytest.mark.skipif(
    getenv("TASK", "") == "cuda", reason="Skip due to differences in implementation details of CUDA version"
)
1357
1358
def test_first_metric_only():
    def fit_and_check(eval_set_names, metric_names, assumed_iteration, first_metric_only):
1359
        params["first_metric_only"] = first_metric_only
1360
        gbm = lgb.LGBMRegressor(**params).fit(**params_fit)
1361
1362
1363
1364
1365
1366
1367
1368
        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])
1369
1370
1371
1372
1373
                expected = assumed_iteration + (
                    params["early_stopping_rounds"]
                    if eval_set_name != "training" and assumed_iteration != gbm.n_estimators
                    else 0
                )
1374
                assert expected == actual
1375
                if eval_set_name != "training":
1376
1377
1378
1379
                    assert assumed_iteration == gbm.best_iteration_
                else:
                    assert gbm.n_estimators == gbm.best_iteration_

1380
    X, y = make_synthetic_regression(n_samples=300)
1381
1382
    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)
1383
1384
1385
1386
1387
1388
1389
1390
1391
    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}
1392

1393
1394
1395
1396
    iter_valid1_l1 = 4
    iter_valid1_l2 = 4
    iter_valid2_l1 = 2
    iter_valid2_l2 = 2
1397
    assert len({iter_valid1_l1, iter_valid1_l2, iter_valid2_l1, iter_valid2_l2}) == 2
1398
1399
1400
1401
1402
1403
    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
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
    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)
1417
1418

    # single eval_set
1419
1420
1421
1422
    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)
1423

1424
1425
1426
    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)
1427

1428
1429
1430
    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)
1431

1432
1433
1434
    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)
1435

1436
1437
1438
    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)
1439

1440
1441
1442
    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)
1443
1444

    # two eval_set
1445
1446
1447
1448
1449
    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)
1450

1451
1452
1453
1454
1455
1456
1457
    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)
1458
1459
1460
1461
1462


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)
1463
1464
1465
1466
1467
1468
1469
1470
1471
    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}],
    )
1472
1473
    for eval_set1, eval_set2 in itertools.combinations(gbm.evals_result_.keys(), 2):
        for metric in gbm.evals_result_[eval_set1]:
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
            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}],
    )
1493
1494
    for eval_set1, eval_set2 in itertools.combinations(gbm_str.evals_result_.keys(), 2):
        for metric in gbm_str.evals_result_[eval_set1]:
1495
1496
1497
1498
1499
1500
            np.testing.assert_raises(
                AssertionError,
                np.testing.assert_allclose,
                gbm_str.evals_result_[eval_set1][metric],
                gbm_str.evals_result_[eval_set2][metric],
            )
1501
1502
    for eval_set in gbm.evals_result_:
        for metric in gbm.evals_result_[eval_set]:
1503
            np.testing.assert_allclose(gbm.evals_result_[eval_set][metric], gbm_str.evals_result_[eval_set][metric])
1504
1505
1506
1507
1508


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)
1509
    init_gbm = lgb.LGBMClassifier(n_estimators=5).fit(X_train, y_train, eval_set=(X_test, y_test))
1510
1511
1512
1513
    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]
1514
1515


1516
1517
def test_actual_number_of_trees():
    X = [[1, 2, 3], [1, 2, 3]]
1518
    y = [1.0, 1.0]
1519
1520
1521
1522
1523
    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
1524
    np_assert_array_equal(gbm.predict(np.array(X) * 10), y, strict=True)
1525
1526


1527
1528
1529
1530
1531
1532
1533
1534
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:
1535
1536
        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):
1537
            check_is_fitted(model)
1538
1539
1540
1541
1542
1543
    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)
1544
1545


1546
@pytest.mark.parametrize("estimator_class", estimator_classes)
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
@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


1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
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


1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
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)


1625
@pytest.mark.parametrize("estimator_class", estimator_classes)
1626
1627
1628
1629
1630
1631
1632
1633
1634
def test_getting_feature_names_in_np_input(estimator_class):
    # input is a numpy array, which doesn't have feature names. LightGBM adds
    # feature names to the fitted model, which is inconsistent with sklearn's behavior
    X, y = load_digits(n_class=2, return_X_y=True)
    params = {"n_estimators": 2, "num_leaves": 7}
    if estimator_class is lgb.LGBMModel:
        model = estimator_class(**{**params, "objective": "binary"})
    else:
        model = estimator_class(**params)
1635
1636
    err_msg = f"This {estimator_class.__name__} instance is not fitted yet. Call 'fit' with appropriate arguments before using this estimator."
    with pytest.raises(lgb.compat.LGBMNotFittedError, match=err_msg):
1637
1638
1639
1640
1641
        check_is_fitted(model)
    if isinstance(model, lgb.LGBMRanker):
        model.fit(X, y, group=[X.shape[0]])
    else:
        model.fit(X, y)
1642
    np_assert_array_equal(model.feature_names_in_, np.array([f"Column_{i}" for i in range(X.shape[1])]), strict=True)
1643
1644


1645
@pytest.mark.parametrize("estimator_class", estimator_classes)
1646
1647
1648
def test_getting_feature_names_in_pd_input(estimator_class):
    X, y = load_digits(n_class=2, return_X_y=True, as_frame=True)
    col_names = X.columns.to_list()
1649
1650
    assert isinstance(col_names, list)
    assert all(isinstance(c, str) for c in col_names), (
1651
1652
        "input data must have feature names for this test to cover the expected functionality"
    )
1653
1654
1655
1656
1657
    params = {"n_estimators": 2, "num_leaves": 7}
    if estimator_class is lgb.LGBMModel:
        model = estimator_class(**{**params, "objective": "binary"})
    else:
        model = estimator_class(**params)
1658
1659
    err_msg = f"This {estimator_class.__name__} instance is not fitted yet. Call 'fit' with appropriate arguments before using this estimator."
    with pytest.raises(lgb.compat.LGBMNotFittedError, match=err_msg):
1660
1661
1662
1663
1664
        check_is_fitted(model)
    if isinstance(model, lgb.LGBMRanker):
        model.fit(X, y, group=[X.shape[0]])
    else:
        model.fit(X, y)
1665
1666
    # strict=False due to dtype mismatch: '<U9' and 'object'
    np_assert_array_equal(model.feature_names_in_, X.columns, strict=False)
1667
1668


1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
# 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"]


1690
1691
1692
1693
@parametrize_with_checks(
    [ExtendedLGBMClassifier(), ExtendedLGBMRegressor(), lgb.LGBMClassifier(), lgb.LGBMRegressor()],
    expected_failed_checks=_get_expected_failed_tests,
)
1694
1695
1696
def test_sklearn_integration(estimator, check):
    estimator.set_params(min_child_samples=1, min_data_in_bin=1)
    check(estimator)
1697
1698


1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
@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__()
1709
    except AttributeError:
1710
        # only the exact error we expected to be raised should be raised
1711
1712
        with pytest.raises(AttributeError, match=r"__sklearn_tags__.* should not be called"):
            est.__sklearn_tags__()
1713
1714
1715
1716
1717
1718
    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
1719
1720
1721
1722
1723
1724
        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"
1725
1726
1727


@pytest.mark.parametrize("task", all_tasks)
1728
1729
def test_training_succeeds_when_data_is_dataframe_and_label_is_column_array(task):
    pd = pytest.importorskip("pandas")
1730
    X, y, g = _create_data(task)
1731
1732
    X = pd.DataFrame(X)
    y_col_array = y.reshape(-1, 1)
1733
    params = {"n_estimators": 1, "num_leaves": 3, "random_state": 0}
1734
    model_factory = task_to_model_factory[task]
1735
1736
1737
    if task == "ranking":
        model_1d = model_factory(**params).fit(X, y, group=g)
        with pytest.warns(UserWarning, match="column-vector"):
1738
            model_2d = model_factory(**params).fit(X, y_col_array, group=g)
1739
1740
1741
    else:
        model_1d = model_factory(**params).fit(X, y)
        with pytest.warns(UserWarning, match="column-vector"):
1742
1743
1744
1745
            model_2d = model_factory(**params).fit(X, y_col_array)

    preds_1d = model_1d.predict(X)
    preds_2d = model_2d.predict(X)
1746
    np_assert_array_equal(preds_1d, preds_2d, strict=True)
1747
1748


1749
@pytest.mark.parametrize("use_weight", [True, False])
1750
def test_multiclass_custom_objective(use_weight):
1751
1752
    centers = [[-4, -4], [4, 4], [-4, 4]]
    X, y = make_blobs(n_samples=1_000, centers=centers, random_state=42)
1753
    weight = np.full_like(y, 2) if use_weight else None
1754
    params = {"n_estimators": 10, "num_leaves": 7}
1755
    builtin_obj_model = lgb.LGBMClassifier(**params)
1756
    builtin_obj_model.fit(X, y, sample_weight=weight)
1757
1758
1759
    builtin_obj_preds = builtin_obj_model.predict_proba(X)

    custom_obj_model = lgb.LGBMClassifier(objective=sklearn_multiclass_custom_objective, **params)
1760
    custom_obj_model.fit(X, y, sample_weight=weight)
1761
1762
1763
1764
1765
    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_)
1766
1767


1768
@pytest.mark.parametrize("use_weight", [True, False])
1769
1770
1771
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)
1772
        return "custom_logloss", loss, False
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783

    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
1784
    params = {"objective": "multiclass", "num_class": 3, "num_leaves": 7}
1785
1786
1787
1788
1789
1790
    model = lgb.LGBMClassifier(**params)
    model.fit(
        X_train,
        y_train,
        sample_weight=weight_train,
        eval_set=[(X_train, y_train), (X_valid, y_valid)],
1791
        eval_names=["train", "valid"],
1792
1793
1794
1795
1796
1797
        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)
1798
1799
    for key, (X, y_true, weight) in zip(["train", "valid"], [train_ds, valid_ds]):
        np.testing.assert_allclose(eval_result[key]["multi_logloss"], eval_result[key]["custom_logloss"])
1800
1801
        y_pred = model.predict_proba(X)
        _, metric_value, _ = custom_eval(y_true, y_pred, weight)
1802
        np.testing.assert_allclose(metric_value, eval_result[key]["custom_logloss"][-1])
1803
1804


1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
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))
1829
1830


1831
@pytest.mark.skipif(not PANDAS_INSTALLED, reason="pandas is not installed")
1832
@pytest.mark.parametrize("task", all_tasks)
1833
def test_validate_features(task):
1834
    X, y, g = _create_data(task, n_features=4)
1835
    features = ["x1", "x2", "x3", "x4"]
1836
1837
    df = pd_DataFrame(X, columns=features)
    model = task_to_model_factory[task](n_estimators=10, num_leaves=15, verbose=-1)
1838
    if task == "ranking":
1839
1840
1841
1842
1843
1844
        model.fit(df, y, group=g)
    else:
        model.fit(df, y)
    assert model.feature_name_ == features

    # try to predict with a different feature
1845
    df2 = df.rename(columns={"x2": "z"})
1846
1847
1848
1849
1850
    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)
1851
1852


1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
# 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]


1899
def run_minimal_test(X_type, y_type, g_type, task, rng):
1900
    X, y, g = _create_data(task, n_samples=2_000)
1901
    weights = np.abs(rng.standard_normal(size=(y.shape[0],)))
1902

1903
    if task in {"binary-classification", "regression", "ranking"}:
1904
        init_score = np.full_like(y, np.mean(y))
1905
    elif task == "multiclass-classification":
1906
1907
1908
1909
1910
        init_score = np.outer(y, np.array([0.1, 0.2, 0.7]))
    else:
        raise ValueError(f"Unrecognized task '{task}'")

    X_valid = X * 2
1911
    if X_type == "list2d":
1912
        X = X.tolist()
1913
    elif X_type == "scipy_csc":
1914
        X = scipy.sparse.csc_matrix(X)
1915
    elif X_type == "scipy_csr":
1916
        X = scipy.sparse.csr_matrix(X)
1917
    elif X_type == "pd_DataFrame":
1918
        X = pd_DataFrame(X)
1919
1920
    elif X_type == "pa_Table":
        X = pa_Table.from_pandas(pd_DataFrame(X))
1921
    elif X_type != "numpy":
1922
1923
        raise ValueError(f"Unrecognized X_type: '{X_type}'")

1924
1925
    # make weights and init_score same types as y, just to avoid
    # a huge number of combinations and therefore test cases
1926
    if y_type == "list1d":
1927
        y = y.tolist()
1928
1929
        weights = weights.tolist()
        init_score = init_score.tolist()
1930
    elif y_type == "pd_DataFrame":
1931
        y = pd_DataFrame(y)
1932
        weights = pd_Series(weights)
1933
        if task == "multiclass-classification":
1934
1935
1936
            init_score = pd_DataFrame(init_score)
        else:
            init_score = pd_Series(init_score)
1937
    elif y_type == "pd_Series":
1938
        y = pd_Series(y)
1939
        weights = pd_Series(weights)
1940
        if task == "multiclass-classification":
1941
1942
1943
            init_score = pd_DataFrame(init_score)
        else:
            init_score = pd_Series(init_score)
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
    elif y_type == "pa_Array":
        y = pa_array(y)
        weights = pa_array(weights)
        if task == "multiclass-classification":
            init_score = pa_Table.from_pandas(pd_DataFrame(init_score))
        else:
            init_score = pa_array(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])
1958
    elif y_type != "numpy":
1959
1960
        raise ValueError(f"Unrecognized y_type: '{y_type}'")

1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
    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_Array":
        g = pa_array(g)
    elif g_type == "pa_ChunkedArray":
        g = pa_chunked_array([g])
    elif g_type != "numpy":
        raise ValueError(f"Unrecognized g_type: '{g_type}'")

1974
    model = task_to_model_factory[task](n_estimators=10, verbose=-1)
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
    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)
1988
1989

    preds = model.predict(X)
1990
    if task == "binary-classification":
1991
        assert accuracy_score(y, preds) >= 0.99
1992
    elif task == "multiclass-classification":
1993
        assert accuracy_score(y, preds) >= 0.99
1994
    elif task == "regression":
1995
        assert r2_score(y, preds) > 0.86
1996
1997
    elif task == "ranking":
        assert spearmanr(preds, y).correlation >= 0.99
1998
1999
2000
2001
    else:
        raise ValueError(f"Unrecognized task: '{task}'")


2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
@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:
2012
        pytest.skip("pandas is not installed")
2013
2014
    if any(t.startswith("pa_") for t in [X_type, y_type]) and not PYARROW_INSTALLED:
        pytest.skip("pyarrow is not installed")
2015

2016
    run_minimal_test(X_type=X_type, y_type=y_type, g_type="numpy", task=task, rng=rng)
2017
2018


2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
@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")
    if any(t.startswith("pa_") for t in [X_type, y_type, g_type]) and not PYARROW_INSTALLED:
        pytest.skip("pyarrow is not installed")
2032

2033
    run_minimal_test(X_type=X_type, y_type=y_type, g_type=g_type, task="ranking", rng=rng)
2034
2035
2036
2037
2038
2039
2040
2041


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))
2042
    y_bin = (rng.random(size=nrows) <= 0.3).astype(np.float64)
2043
2044
2045
2046
2047
2048
2049
2050
    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"