test_sklearn.py 85.8 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
28
29
30
31
    PANDAS_INSTALLED,
    _sklearn_version,
    pd_DataFrame,
    pd_Series,
)
32

33
from .utils import (
34
    assert_silent,
35
36
37
38
39
40
41
42
43
    load_breast_cancer,
    load_digits,
    load_iris,
    load_linnerud,
    make_ranking,
    make_synthetic_regression,
    sklearn_multiclass_custom_objective,
    softmax,
)
44

45
46
47
SKLEARN_MAJOR, SKLEARN_MINOR, *_ = _sklearn_version.split(".")
SKLEARN_VERSION_GTE_1_6 = (int(SKLEARN_MAJOR), int(SKLEARN_MINOR)) >= (1, 6)

48
decreasing_generator = itertools.count(0, -1)
49
estimator_classes = (lgb.LGBMModel, lgb.LGBMClassifier, lgb.LGBMRegressor, lgb.LGBMRanker)
50
task_to_model_factory = {
51
52
53
54
    "ranking": lgb.LGBMRanker,
    "binary-classification": lgb.LGBMClassifier,
    "multiclass-classification": lgb.LGBMClassifier,
    "regression": lgb.LGBMRegressor,
55
}
56
all_tasks = tuple(task_to_model_factory.keys())
57
58


59
def _create_data(task, n_samples=100, n_features=4):
60
    if task == "ranking":
61
        X, y, g = make_ranking(n_features=4, n_samples=n_samples)
62
        g = np.bincount(g)
63
64
    elif task.endswith("classification"):
        if task == "binary-classification":
65
            centers = 2
66
        elif task == "multiclass-classification":
67
68
            centers = 3
        else:
69
            raise ValueError(f"Unknown classification task '{task}'")
70
        X, y = make_blobs(n_samples=n_samples, n_features=n_features, centers=centers, random_state=42)
71
        g = None
72
    elif task == "regression":
73
        X, y = make_synthetic_regression(n_samples=n_samples, n_features=n_features)
74
75
        g = None
    return X, y, g
wxchan's avatar
wxchan committed
76

wxchan's avatar
wxchan committed
77

78
79
80
81
82
class UnpicklableCallback:
    def __reduce__(self):
        raise Exception("This class in not picklable")

    def __call__(self, env):
83
        env.model.attr_set_inside_callback = env.iteration * 10
84
85


86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
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)


110
def custom_asymmetric_obj(y_true, y_pred):
111
    residual = (y_true - y_pred).astype(np.float64)
112
113
114
115
116
    grad = np.where(residual < 0, -2 * 10.0 * residual, -2 * residual)
    hess = np.where(residual < 0, 2 * 10.0, 2.0)
    return grad, hess


117
def objective_ls(y_true, y_pred):
118
    grad = y_pred - y_true
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
    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):
135
    return "error", 0, False
136
137
138


def decreasing_metric(y_true, y_pred):
139
    return ("decreasing_metric", next(decreasing_generator), False)
140
141


142
def mse(y_true, y_pred):
143
    return "custom MSE", mean_squared_error(y_true, y_pred), False
144
145


146
147
148
149
150
151
152
153
154
155
156
157
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)])


158
159
160
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)
161
    gbm = lgb.LGBMClassifier(n_estimators=50, verbose=-1)
162
    gbm.fit(X_train, y_train, eval_set=[(X_test, y_test)], callbacks=[lgb.early_stopping(5)])
163
164
    ret = log_loss(y_test, gbm.predict_proba(X_test))
    assert ret < 0.12
165
    assert gbm.evals_result_["valid_0"]["binary_logloss"][gbm.best_iteration_ - 1] == pytest.approx(ret)
166
167
168


def test_regression():
169
    X, y = make_synthetic_regression()
170
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, random_state=42)
171
    gbm = lgb.LGBMRegressor(n_estimators=50, verbose=-1)
172
    gbm.fit(X_train, y_train, eval_set=[(X_test, y_test)], callbacks=[lgb.early_stopping(5)])
173
    ret = mean_squared_error(y_test, gbm.predict(X_test))
174
    assert ret < 174
175
    assert gbm.evals_result_["valid_0"]["l2"][gbm.best_iteration_ - 1] == pytest.approx(ret)
176
177


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


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


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


239
def test_eval_at_aliases():
240
241
242
243
244
245
    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"):
246
247
248
        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])
249
        assert list(gbm.evals_result_["valid_0"].keys()) == ["ndcg@1", "ndcg@2", "ndcg@3", "ndcg@9"]
250
251


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


282
def test_regression_with_custom_objective():
283
    X, y = make_synthetic_regression()
284
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, random_state=42)
285
    gbm = lgb.LGBMRegressor(n_estimators=50, verbose=-1, objective=objective_ls)
286
    gbm.fit(X_train, y_train, eval_set=[(X_test, y_test)], callbacks=[lgb.early_stopping(5)])
287
    ret = mean_squared_error(y_test, gbm.predict(X_test))
288
    assert ret < 174
289
    assert gbm.evals_result_["valid_0"]["l2"][gbm.best_iteration_ - 1] == pytest.approx(ret)
290
291
292
293
294


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


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)
317
318
319
320
    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
    )
321
322
323
    clf.fit(X_train, y_train)
    score = clf.score(X_test, y_test)
    assert score >= 0.8
324
    assert score <= 1.0
325
    assert clf.n_features_in_ == 4  # number of input features
326
327
    assert len(clf.named_estimators_["gbm1"].feature_importances_) == 4
    assert clf.named_estimators_["gbm1"].n_features_in_ == clf.named_estimators_["gbm2"].n_features_in_
328
329
    assert clf.final_estimator_.n_features_in_ == 10  # number of concatenated features
    assert len(clf.final_estimator_.feature_importances_) == 10
330
331
    assert all(clf.named_estimators_["gbm1"].classes_ == clf.named_estimators_["gbm2"].classes_)
    assert all(clf.classes_ == clf.named_estimators_["gbm1"].classes_)
332
333
334


def test_stacking_regressor():
335
336
337
    X, y = make_synthetic_regression(n_samples=200)
    n_features = X.shape[1]
    n_input_models = 2
338
    X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)
339
340
    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)
341
342
343
    reg.fit(X_train, y_train)
    score = reg.score(X_test, y_test)
    assert score >= 0.2
344
    assert score <= 1.0
345
    assert reg.n_features_in_ == n_features  # number of input features
346
347
    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_
348
349
    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
350
351
352
353
354


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


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


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


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

    gbm_clone = clone(gbm)
480
481
482
483

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

487
488
489
490
491
492
    # 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()

493
    X, y = load_digits(n_class=2, return_X_y=True)
494
    clf = lgb.LGBMClassifier(n_estimators=10, verbose=-1)
495
    clf.fit(X, y)
496
497
498
499
500
501
    assert sorted(clf.classes_) == [0, 1]
    assert clf.n_classes_ == 2
    assert isinstance(clf.booster_, lgb.Booster)
    assert isinstance(clf.feature_importances_, np.ndarray)


502
503
504
505
506
507
508
509
510
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
@pytest.mark.parametrize("estimator", (lgb.LGBMClassifier, lgb.LGBMRegressor, lgb.LGBMRanker))
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


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


717
718
719
720
def test_non_serializable_objects_in_callbacks(tmp_path):
    unpicklable_callback = UnpicklableCallback()

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

723
    X, y = make_synthetic_regression()
724
725
    gbm = lgb.LGBMRegressor(n_estimators=5)
    gbm.fit(X, y, callbacks=[unpicklable_callback])
726
    assert gbm.booster_.attr_set_inside_callback == 40
727
728


729
730
@pytest.mark.parametrize("rng_constructor", [np.random.RandomState, np.random.default_rng])
def test_random_state_object(rng_constructor):
731
732
    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)
733
734
    state1 = rng_constructor(123)
    state2 = rng_constructor(123)
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
    clf1 = lgb.LGBMClassifier(n_estimators=10, subsample=0.5, subsample_freq=1, random_state=state1)
    clf2 = lgb.LGBMClassifier(n_estimators=10, subsample=0.5, subsample_freq=1, random_state=state2)
    # Test if random_state is properly stored
    assert clf1.random_state is state1
    assert clf2.random_state is state2
    # Test if two random states produce identical models
    clf1.fit(X_train, y_train)
    clf2.fit(X_train, y_train)
    y_pred1 = clf1.predict(X_test, raw_score=True)
    y_pred2 = clf2.predict(X_test, raw_score=True)
    np.testing.assert_allclose(y_pred1, y_pred2)
    np.testing.assert_array_equal(clf1.feature_importances_, clf2.feature_importances_)
    df1 = clf1.booster_.model_to_string(num_iteration=0)
    df2 = clf2.booster_.model_to_string(num_iteration=0)
    assert df1 == df2
    # Test if subsequent fits sample from random_state object and produce different models
    clf1.fit(X_train, y_train)
    y_pred1_refit = clf1.predict(X_test, raw_score=True)
    df3 = clf1.booster_.model_to_string(num_iteration=0)
    assert clf1.random_state is state1
    assert clf2.random_state is state2
    with pytest.raises(AssertionError):
        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)
773
    clf.set_params(importance_type="split")
774
    importances_split = clf.feature_importances_
775
    clf.set_params(importance_type="gain")
776
777
778
779
780
781
782
    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


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


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

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

947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
    # 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)

966

967
968
969
970
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)

971
    predict_params = {"pred_early_stop": True, "pred_early_stop_margin": 1.0}
972

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

975
976
977
    y_preds_params_in_predict = (
        lgb.LGBMClassifier(verbose=-1).fit(X_train, y_train).predict(X_test, raw_score=True, **predict_params)
    )
978
979
980
    with pytest.raises(AssertionError):
        np.testing.assert_allclose(y_preds_no_params, y_preds_params_in_predict)

981
982
983
984
985
986
    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)
    )
987
988
    np.testing.assert_allclose(y_preds_params_in_predict, y_preds_params_in_set_params_before_fit)

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

997
998
999
    y_preds_params_in_init = (
        lgb.LGBMClassifier(verbose=-1, **predict_params).fit(X_train, y_train).predict(X_test, raw_score=True)
    )
1000
1001
1002
    np.testing.assert_allclose(y_preds_params_in_predict, y_preds_params_in_init)

    # test that params passed in predict have higher priority
1003
1004
1005
1006
1007
    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)
    )
1008
1009
1010
    np.testing.assert_allclose(y_preds_no_params, y_preds_params_overwritten)


1011
def test_evaluate_train_set():
1012
    X, y = make_synthetic_regression()
1013
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, random_state=42)
1014
    gbm = lgb.LGBMRegressor(n_estimators=10, verbose=-1)
1015
    gbm.fit(X_train, y_train, eval_set=[(X_train, y_train), (X_test, y_test)])
1016
    assert len(gbm.evals_result_) == 2
1017
1018
1019
1020
1021
1022
    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"]
1023
1024
1025


def test_metrics():
1026
1027
    X, y = make_synthetic_regression()
    y = abs(y)
1028
1029
    params = {"n_estimators": 2, "verbose": -1}
    params_fit = {"X": X, "y": y, "eval_set": (X, y)}
1030
1031
1032
1033

    # no custom objective, no custom metric
    # default metric
    gbm = lgb.LGBMRegressor(**params).fit(**params_fit)
1034
1035
    assert len(gbm.evals_result_["training"]) == 1
    assert "l2" in gbm.evals_result_["training"]
1036
1037

    # non-default metric
1038
1039
1040
    gbm = lgb.LGBMRegressor(metric="mape", **params).fit(**params_fit)
    assert len(gbm.evals_result_["training"]) == 1
    assert "mape" in gbm.evals_result_["training"]
1041
1042

    # no metric
1043
    gbm = lgb.LGBMRegressor(metric="None", **params).fit(**params_fit)
1044
    assert gbm.evals_result_ == {}
1045
1046

    # non-default metric in eval_metric
1047
1048
1049
1050
    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"]
1051
1052

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

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

    # non-default metric with multiple metrics in eval_metric for LGBMClassifier
    X_classification, y_classification = load_breast_cancer(return_X_y=True)
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
    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"]
1078
1079

    # default metric for non-default objective
1080
1081
1082
    gbm = lgb.LGBMRegressor(objective="regression_l1", **params).fit(**params_fit)
    assert len(gbm.evals_result_["training"]) == 1
    assert "l1" in gbm.evals_result_["training"]
1083
1084

    # non-default metric for non-default objective
1085
1086
1087
    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"]
1088
1089

    # no metric
1090
    gbm = lgb.LGBMRegressor(objective="regression_l1", metric="None", **params).fit(**params_fit)
1091
    assert gbm.evals_result_ == {}
1092
1093

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

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

    # non-default metric with multiple metrics in eval_metric for non-default objective
1106
1107
1108
1109
1110
1111
1112
    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"]
1113
1114
1115
1116

    # custom objective, no custom metric
    # default regression metric for custom objective
    gbm = lgb.LGBMRegressor(objective=custom_dummy_obj, **params).fit(**params_fit)
1117
1118
    assert len(gbm.evals_result_["training"]) == 1
    assert "l2" in gbm.evals_result_["training"]
1119
1120

    # non-default regression metric for custom objective
1121
1122
1123
    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"]
1124
1125

    # multiple regression metrics for custom objective
1126
1127
1128
1129
    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"]
1130
1131

    # no metric
1132
    gbm = lgb.LGBMRegressor(objective=custom_dummy_obj, metric="None", **params).fit(**params_fit)
1133
    assert gbm.evals_result_ == {}
1134
1135

    # default regression metric with non-default metric in eval_metric for custom objective
1136
1137
1138
1139
    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"]
1140
1141

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

    # multiple regression metrics with metric in eval_metric for custom objective
1148
1149
1150
1151
1152
1153
1154
    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"]
1155
1156

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

    # no custom objective, custom metric
    # default metric with custom metric
    gbm = lgb.LGBMRegressor(**params).fit(eval_metric=constant_metric, **params_fit)
1169
1170
1171
    assert len(gbm.evals_result_["training"]) == 2
    assert "l2" in gbm.evals_result_["training"]
    assert "error" in gbm.evals_result_["training"]
1172
1173

    # non-default metric with custom metric
1174
1175
1176
1177
    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"]
1178
1179

    # multiple metrics with custom metric
1180
1181
1182
1183
1184
    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"]
1185
1186

    # custom metric (disable default metric)
1187
1188
1189
    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"]
1190
1191

    # default metric for non-default objective with custom metric
1192
1193
1194
1195
    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"]
1196
1197

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

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

    # custom metric (disable default metric for non-default objective)
1215
1216
1217
1218
1219
    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"]
1220
1221
1222

    # custom objective, custom metric
    # custom metric for custom objective
1223
1224
1225
    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"]
1226
1227

    # non-default regression metric with custom metric for custom objective
1228
1229
1230
1231
1232
1233
    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"]
1234
1235

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

    X, y = load_digits(n_class=3, return_X_y=True)
1245
    params_fit = {"X": X, "y": y, "eval_set": (X, y)}
1246
1247

    # default metric and invalid binary metric is replaced with multiclass alternative
1248
1249
1250
1251
    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"]
1252

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

    # default metric for non-default multiclass objective
    # and invalid binary metric is replaced with multiclass alternative
1262
1263
1264
1265
1266
    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"]
1267
1268

    X, y = load_digits(n_class=2, return_X_y=True)
1269
    params_fit = {"X": X, "y": y, "eval_set": (X, y)}
1270
1271

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

    # invalid multiclass metric is replaced with binary alternative for custom objective
1278
1279
1280
    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"]
1281

1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
    # 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"]

1296
1297
1298
1299

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

1300
1301
    params = {"n_estimators": 2, "verbose": -1, "objective": "binary", "metric": "binary_logloss"}
    params_fit = {"X": X, "y": y, "eval_set": (X, y)}
1302
1303
1304

    # Verify that can receive a list of metrics, only callable
    gbm = lgb.LGBMClassifier(**params).fit(eval_metric=[constant_metric, decreasing_metric], **params_fit)
1305
1306
1307
1308
    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"]
1309
1310

    # Verify that can receive a list of custom and built-in metrics
1311
1312
1313
1314
1315
1316
    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"]
1317
1318
1319

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

    # Verify that can receive a list of metrics, only built-in
1324
1325
1326
    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"]
1327
1328

    # Verify that eval_metric is robust to receiving a list with None
1329
1330
1331
    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"]
1332
1333


1334
def test_nan_handle(rng):
1335
1336
    nrows = 100
    ncols = 10
1337
1338
    X = rng.standard_normal(size=(nrows, ncols))
    y = rng.standard_normal(size=(nrows,)) + np.full(nrows, 1e30)
1339
    weight = np.zeros(nrows)
1340
1341
    params = {"n_estimators": 20, "verbose": -1}
    params_fit = {"X": X, "y": y, "sample_weight": weight, "eval_set": (X, y), "callbacks": [lgb.early_stopping(5)]}
1342
    gbm = lgb.LGBMRegressor(**params).fit(**params_fit)
1343
    np.testing.assert_allclose(gbm.evals_result_["training"]["l2"], np.nan)
1344
1345


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

1372
    X, y = make_synthetic_regression(n_samples=300)
1373
1374
    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)
1375
1376
1377
1378
1379
1380
1381
1382
1383
    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}
1384

1385
1386
1387
1388
    iter_valid1_l1 = 4
    iter_valid1_l2 = 4
    iter_valid2_l1 = 2
    iter_valid2_l2 = 2
1389
    assert len({iter_valid1_l1, iter_valid1_l2, iter_valid2_l1, iter_valid2_l2}) == 2
1390
1391
1392
1393
1394
1395
    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
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
    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)
1409
1410

    # single eval_set
1411
1412
1413
1414
    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)
1415

1416
1417
1418
    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)
1419

1420
1421
1422
    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)
1423

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

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

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

    # two eval_set
1437
1438
1439
1440
1441
    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)
1442

1443
1444
1445
1446
1447
1448
1449
    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)
1450
1451
1452
1453
1454


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


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)
1501
    init_gbm = lgb.LGBMClassifier(n_estimators=5).fit(X_train, y_train, eval_set=(X_test, y_test))
1502
1503
1504
1505
    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]
1506
1507


1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
def test_actual_number_of_trees():
    X = [[1, 2, 3], [1, 2, 3]]
    y = [1, 1]
    n_estimators = 5
    gbm = lgb.LGBMRegressor(n_estimators=n_estimators).fit(X, y)
    assert gbm.n_estimators == n_estimators
    assert gbm.n_estimators_ == 1
    assert gbm.n_iter_ == 1
    np.testing.assert_array_equal(gbm.predict(np.array(X) * 10), y)


1519
1520
1521
1522
1523
1524
1525
1526
1527
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:
        with pytest.raises(lgb.compat.LGBMNotFittedError):
1528
            check_is_fitted(model)
1529
1530
1531
1532
1533
1534
    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)
1535
1536


1537
@pytest.mark.parametrize("estimator_class", estimator_classes)
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
@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


1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
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


1564
1565
1566
1567
1568
1569
1570
1571
1572
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
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)


1616
@pytest.mark.parametrize("estimator_class", estimator_classes)
1617
1618
1619
1620
1621
1622
1623
1624
1625
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)
    with pytest.raises(lgb.compat.LGBMNotFittedError):
        check_is_fitted(model)
    if isinstance(model, lgb.LGBMRanker):
        model.fit(X, y, group=[X.shape[0]])
    else:
        model.fit(X, y)
    np.testing.assert_array_equal(model.feature_names_in_, np.array([f"Column_{i}" for i in range(X.shape[1])]))


1635
@pytest.mark.parametrize("estimator_class", estimator_classes)
1636
1637
1638
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()
1639
1640
1641
    assert isinstance(col_names, list) and all(isinstance(c, str) for c in col_names), (
        "input data must have feature names for this test to cover the expected functionality"
    )
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
    params = {"n_estimators": 2, "num_leaves": 7}
    if estimator_class is lgb.LGBMModel:
        model = estimator_class(**{**params, "objective": "binary"})
    else:
        model = estimator_class(**params)
    with pytest.raises(lgb.compat.LGBMNotFittedError):
        check_is_fitted(model)
    if isinstance(model, lgb.LGBMRanker):
        model.fit(X, y, group=[X.shape[0]])
    else:
        model.fit(X, y)
    np.testing.assert_array_equal(model.feature_names_in_, X.columns)


1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
# 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"]


1677
1678
1679
1680
@parametrize_with_checks(
    [ExtendedLGBMClassifier(), ExtendedLGBMRegressor(), lgb.LGBMClassifier(), lgb.LGBMRegressor()],
    expected_failed_checks=_get_expected_failed_tests,
)
1681
1682
1683
def test_sklearn_integration(estimator, check):
    estimator.set_params(min_child_samples=1, min_data_in_bin=1)
    check(estimator)
1684
1685


1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
@pytest.mark.parametrize("estimator_class", estimator_classes)
def test_sklearn_tags_should_correctly_reflect_lightgbm_specific_values(estimator_class):
    est = estimator_class()
    more_tags = est._more_tags()
    err_msg = "List of supported X_types has changed. Update LGBMModel.__sklearn_tags__() to match."
    assert more_tags["X_types"] == ["2darray", "sparse", "1dlabels"], err_msg
    # the try-except part of this should be removed once lightgbm's
    # minimum supported scikit-learn version is at least 1.6
    try:
        sklearn_tags = est.__sklearn_tags__()
    except AttributeError as err:
        # only the exact error we expected to be raised should be raised
        assert bool(re.search(r"__sklearn_tags__.* should not be called", str(err)))
    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
1705
1706
1707
1708
1709
1710
        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"
1711
1712
1713


@pytest.mark.parametrize("task", all_tasks)
1714
1715
def test_training_succeeds_when_data_is_dataframe_and_label_is_column_array(task):
    pd = pytest.importorskip("pandas")
1716
    X, y, g = _create_data(task)
1717
1718
    X = pd.DataFrame(X)
    y_col_array = y.reshape(-1, 1)
1719
    params = {"n_estimators": 1, "num_leaves": 3, "random_state": 0}
1720
    model_factory = task_to_model_factory[task]
1721
1722
    with pytest.warns(UserWarning, match="column-vector"):
        if task == "ranking":
1723
1724
1725
1726
1727
1728
1729
1730
1731
            model_1d = model_factory(**params).fit(X, y, group=g)
            model_2d = model_factory(**params).fit(X, y_col_array, group=g)
        else:
            model_1d = model_factory(**params).fit(X, y)
            model_2d = model_factory(**params).fit(X, y_col_array)

    preds_1d = model_1d.predict(X)
    preds_2d = model_2d.predict(X)
    np.testing.assert_array_equal(preds_1d, preds_2d)
1732
1733


1734
@pytest.mark.parametrize("use_weight", [True, False])
1735
def test_multiclass_custom_objective(use_weight):
1736
1737
    centers = [[-4, -4], [4, 4], [-4, 4]]
    X, y = make_blobs(n_samples=1_000, centers=centers, random_state=42)
1738
    weight = np.full_like(y, 2) if use_weight else None
1739
    params = {"n_estimators": 10, "num_leaves": 7}
1740
    builtin_obj_model = lgb.LGBMClassifier(**params)
1741
    builtin_obj_model.fit(X, y, sample_weight=weight)
1742
1743
1744
    builtin_obj_preds = builtin_obj_model.predict_proba(X)

    custom_obj_model = lgb.LGBMClassifier(objective=sklearn_multiclass_custom_objective, **params)
1745
    custom_obj_model.fit(X, y, sample_weight=weight)
1746
1747
1748
1749
1750
    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_)
1751
1752


1753
@pytest.mark.parametrize("use_weight", [True, False])
1754
1755
1756
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)
1757
        return "custom_logloss", loss, False
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768

    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
1769
    params = {"objective": "multiclass", "num_class": 3, "num_leaves": 7}
1770
1771
1772
1773
1774
1775
    model = lgb.LGBMClassifier(**params)
    model.fit(
        X_train,
        y_train,
        sample_weight=weight_train,
        eval_set=[(X_train, y_train), (X_valid, y_valid)],
1776
        eval_names=["train", "valid"],
1777
1778
1779
1780
1781
1782
        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)
1783
1784
    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"])
1785
1786
        y_pred = model.predict_proba(X)
        _, metric_value, _ = custom_eval(y_true, y_pred, weight)
1787
        np.testing.assert_allclose(metric_value, eval_result[key]["custom_logloss"][-1])
1788
1789


1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
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))
1814
1815


1816
@pytest.mark.skipif(not PANDAS_INSTALLED, reason="pandas is not installed")
1817
@pytest.mark.parametrize("task", all_tasks)
1818
def test_validate_features(task):
1819
    X, y, g = _create_data(task, n_features=4)
1820
    features = ["x1", "x2", "x3", "x4"]
1821
1822
    df = pd_DataFrame(X, columns=features)
    model = task_to_model_factory[task](n_estimators=10, num_leaves=15, verbose=-1)
1823
    if task == "ranking":
1824
1825
1826
1827
1828
1829
        model.fit(df, y, group=g)
    else:
        model.fit(df, y)
    assert model.feature_name_ == features

    # try to predict with a different feature
1830
    df2 = df.rename(columns={"x2": "z"})
1831
1832
1833
1834
1835
    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)
1836
1837


1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
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
# 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]


1884
@pytest.mark.parametrize("X_type", ["list2d", "numpy", "scipy_csc", "scipy_csr", "pd_DataFrame"])
1885
1886
@pytest.mark.parametrize("y_type", ["list1d", "numpy", "pd_Series", "pd_DataFrame"])
@pytest.mark.parametrize("task", ["binary-classification", "multiclass-classification", "regression"])
1887
def test_classification_and_regression_minimally_work_with_all_all_accepted_data_types(X_type, y_type, task, rng):
1888
    if any(t.startswith("pd_") for t in [X_type, y_type]) and not PANDAS_INSTALLED:
1889
        pytest.skip("pandas is not installed")
1890
    X, y, g = _create_data(task, n_samples=2_000)
1891
    weights = np.abs(rng.standard_normal(size=(y.shape[0],)))
1892

1893
    if task == "binary-classification" or task == "regression":
1894
        init_score = np.full_like(y, np.mean(y))
1895
    elif task == "multiclass-classification":
1896
1897
1898
1899
1900
        init_score = np.outer(y, np.array([0.1, 0.2, 0.7]))
    else:
        raise ValueError(f"Unrecognized task '{task}'")

    X_valid = X * 2
1901
    if X_type == "list2d":
1902
        X = X.tolist()
1903
    elif X_type == "scipy_csc":
1904
        X = scipy.sparse.csc_matrix(X)
1905
    elif X_type == "scipy_csr":
1906
        X = scipy.sparse.csr_matrix(X)
1907
    elif X_type == "pd_DataFrame":
1908
        X = pd_DataFrame(X)
1909
    elif X_type != "numpy":
1910
1911
        raise ValueError(f"Unrecognized X_type: '{X_type}'")

1912
1913
    # make weights and init_score same types as y, just to avoid
    # a huge number of combinations and therefore test cases
1914
    if y_type == "list1d":
1915
        y = y.tolist()
1916
1917
        weights = weights.tolist()
        init_score = init_score.tolist()
1918
    elif y_type == "pd_DataFrame":
1919
        y = pd_DataFrame(y)
1920
        weights = pd_Series(weights)
1921
        if task == "multiclass-classification":
1922
1923
1924
            init_score = pd_DataFrame(init_score)
        else:
            init_score = pd_Series(init_score)
1925
    elif y_type == "pd_Series":
1926
        y = pd_Series(y)
1927
        weights = pd_Series(weights)
1928
        if task == "multiclass-classification":
1929
1930
1931
            init_score = pd_DataFrame(init_score)
        else:
            init_score = pd_Series(init_score)
1932
    elif y_type != "numpy":
1933
1934
1935
        raise ValueError(f"Unrecognized y_type: '{y_type}'")

    model = task_to_model_factory[task](n_estimators=10, verbose=-1)
1936
1937
1938
1939
1940
1941
1942
    model.fit(
        X=X,
        y=y,
        sample_weight=weights,
        init_score=init_score,
        eval_set=[(X_valid, y)],
        eval_sample_weight=[weights],
1943
        eval_init_score=[init_score],
1944
    )
1945
1946

    preds = model.predict(X)
1947
    if task == "binary-classification":
1948
        assert accuracy_score(y, preds) >= 0.99
1949
    elif task == "multiclass-classification":
1950
        assert accuracy_score(y, preds) >= 0.99
1951
    elif task == "regression":
1952
1953
1954
1955
1956
        assert r2_score(y, preds) > 0.86
    else:
        raise ValueError(f"Unrecognized task: '{task}'")


1957
@pytest.mark.parametrize("X_type", ["list2d", "numpy", "scipy_csc", "scipy_csr", "pd_DataFrame"])
1958
1959
@pytest.mark.parametrize("y_type", ["list1d", "numpy", "pd_DataFrame", "pd_Series"])
@pytest.mark.parametrize("g_type", ["list1d_float", "list1d_int", "numpy", "pd_Series"])
1960
def test_ranking_minimally_works_with_all_all_accepted_data_types(X_type, y_type, g_type, rng):
1961
    if any(t.startswith("pd_") for t in [X_type, y_type, g_type]) and not PANDAS_INSTALLED:
1962
1963
        pytest.skip("pandas is not installed")
    X, y, g = _create_data(task="ranking", n_samples=1_000)
1964
    weights = np.abs(rng.standard_normal(size=(y.shape[0],)))
1965
1966
1967
    init_score = np.full_like(y, np.mean(y))
    X_valid = X * 2

1968
    if X_type == "list2d":
1969
        X = X.tolist()
1970
    elif X_type == "scipy_csc":
1971
        X = scipy.sparse.csc_matrix(X)
1972
    elif X_type == "scipy_csr":
1973
        X = scipy.sparse.csr_matrix(X)
1974
    elif X_type == "pd_DataFrame":
1975
        X = pd_DataFrame(X)
1976
    elif X_type != "numpy":
1977
1978
        raise ValueError(f"Unrecognized X_type: '{X_type}'")

1979
1980
    # make weights and init_score same types as y, just to avoid
    # a huge number of combinations and therefore test cases
1981
    if y_type == "list1d":
1982
        y = y.tolist()
1983
1984
        weights = weights.tolist()
        init_score = init_score.tolist()
1985
    elif y_type == "pd_DataFrame":
1986
        y = pd_DataFrame(y)
1987
1988
        weights = pd_Series(weights)
        init_score = pd_Series(init_score)
1989
    elif y_type == "pd_Series":
1990
        y = pd_Series(y)
1991
1992
        weights = pd_Series(weights)
        init_score = pd_Series(init_score)
1993
    elif y_type != "numpy":
1994
1995
        raise ValueError(f"Unrecognized y_type: '{y_type}'")

1996
    if g_type == "list1d_float":
1997
        g = g.astype("float").tolist()
1998
    elif g_type == "list1d_int":
1999
        g = g.astype("int").tolist()
2000
    elif g_type == "pd_Series":
2001
        g = pd_Series(g)
2002
    elif g_type != "numpy":
2003
2004
        raise ValueError(f"Unrecognized g_type: '{g_type}'")

2005
    model = task_to_model_factory["ranking"](n_estimators=10, verbose=-1)
2006
2007
2008
2009
2010
2011
2012
2013
2014
    model.fit(
        X=X,
        y=y,
        sample_weight=weights,
        init_score=init_score,
        group=g,
        eval_set=[(X_valid, y)],
        eval_sample_weight=[weights],
        eval_init_score=[init_score],
2015
        eval_group=[g],
2016
    )
2017
2018
    preds = model.predict(X)
    assert spearmanr(preds, y).correlation >= 0.99
2019
2020
2021
2022
2023
2024
2025
2026


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))
2027
    y_bin = (rng.random(size=nrows) <= 0.3).astype(np.float64)
2028
2029
2030
2031
2032
2033
2034
2035
    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"