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

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

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

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

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

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


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

wxchan's avatar
wxchan committed
84

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

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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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

    gbm_clone = clone(gbm)
487
488
489
490

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

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

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


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
689
690
691
692
693
694
695
@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


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


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

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

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


736
737
@pytest.mark.parametrize("rng_constructor", [np.random.RandomState, np.random.default_rng])
def test_random_state_object(rng_constructor):
738
739
    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)
740
741
    state1 = rng_constructor(123)
    state2 = rng_constructor(123)
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
773
774
775
776
777
778
779
    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)
780
    clf.set_params(importance_type="split")
781
    importances_split = clf.feature_importances_
782
    clf.set_params(importance_type="gain")
783
784
785
786
787
788
789
    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


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


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

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

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

973

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

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

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

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

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

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

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

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


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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1303
1304
1305
1306

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

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

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

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

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

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

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


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


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

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

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

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

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

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

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

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

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

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

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


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


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


1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
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)


1526
1527
1528
1529
1530
1531
1532
1533
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:
1534
1535
        err_msg = f"This {type(model).__name__} instance is not fitted yet. Call 'fit' with appropriate arguments before using this estimator."
        with pytest.raises(lgb.compat.LGBMNotFittedError, match=err_msg):
1536
            check_is_fitted(model)
1537
1538
1539
1540
1541
1542
    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)
1543
1544


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


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


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
1616
1617
1618
1619
1620
1621
1622
1623
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)


1624
@pytest.mark.parametrize("estimator_class", estimator_classes)
1625
1626
1627
1628
1629
1630
1631
1632
1633
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)
1634
1635
    err_msg = f"This {estimator_class.__name__} instance is not fitted yet. Call 'fit' with appropriate arguments before using this estimator."
    with pytest.raises(lgb.compat.LGBMNotFittedError, match=err_msg):
1636
1637
1638
1639
1640
1641
1642
1643
        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])]))


1644
@pytest.mark.parametrize("estimator_class", estimator_classes)
1645
1646
1647
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()
1648
1649
1650
    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"
    )
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)
1656
1657
    err_msg = f"This {estimator_class.__name__} instance is not fitted yet. Call 'fit' with appropriate arguments before using this estimator."
    with pytest.raises(lgb.compat.LGBMNotFittedError, match=err_msg):
1658
1659
1660
1661
1662
1663
1664
1665
        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)


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


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


1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
@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
1715
1716
1717
1718
1719
1720
        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"
1721
1722
1723


@pytest.mark.parametrize("task", all_tasks)
1724
1725
def test_training_succeeds_when_data_is_dataframe_and_label_is_column_array(task):
    pd = pytest.importorskip("pandas")
1726
    X, y, g = _create_data(task)
1727
1728
    X = pd.DataFrame(X)
    y_col_array = y.reshape(-1, 1)
1729
    params = {"n_estimators": 1, "num_leaves": 3, "random_state": 0}
1730
    model_factory = task_to_model_factory[task]
1731
1732
    with pytest.warns(UserWarning, match="column-vector"):
        if task == "ranking":
1733
1734
1735
1736
1737
1738
1739
1740
1741
            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)
1742
1743


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

    custom_obj_model = lgb.LGBMClassifier(objective=sklearn_multiclass_custom_objective, **params)
1755
    custom_obj_model.fit(X, y, sample_weight=weight)
1756
1757
1758
1759
1760
    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_)
1761
1762


1763
@pytest.mark.parametrize("use_weight", [True, False])
1764
1765
1766
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)
1767
        return "custom_logloss", loss, False
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778

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


1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
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))
1824
1825


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

    # try to predict with a different feature
1840
    df2 = df.rename(columns={"x2": "z"})
1841
1842
1843
1844
1845
    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)
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
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
# 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]


1894
def run_minimal_test(X_type, y_type, g_type, task, rng):
1895
    X, y, g = _create_data(task, n_samples=2_000)
1896
    weights = np.abs(rng.standard_normal(size=(y.shape[0],)))
1897

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

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

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

1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
    if g_type == "list1d_float":
        g = g.astype("float").tolist()
    elif g_type == "list1d_int":
        g = g.astype("int").tolist()
    elif g_type == "pd_Series":
        g = pd_Series(g)
    elif g_type == "pa_Array":
        g = pa_array(g)
    elif g_type == "pa_ChunkedArray":
        g = pa_chunked_array([g])
    elif g_type != "numpy":
        raise ValueError(f"Unrecognized g_type: '{g_type}'")

1969
    model = task_to_model_factory[task](n_estimators=10, verbose=-1)
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
    params_fit = {
        "X": X,
        "y": y,
        "sample_weight": weights,
        "init_score": init_score,
        "eval_set": [(X_valid, y)],
        "eval_sample_weight": [weights],
        "eval_init_score": [init_score],
    }
    if task == "ranking":
        params_fit["group"] = g
        params_fit["eval_group"] = [g]
    model.fit(**params_fit)
1983
1984

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


1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
@pytest.mark.parametrize("X_type", all_x_types)
@pytest.mark.parametrize("y_type", all_y_types)
@pytest.mark.parametrize("task", [t for t in all_tasks if t != "ranking"])
def test_classification_and_regression_minimally_work_with_all_accepted_data_types(
    X_type,
    y_type,
    task,
    rng,
):
    if any(t.startswith("pd_") for t in [X_type, y_type]) and not PANDAS_INSTALLED:
2007
        pytest.skip("pandas is not installed")
2008
2009
    if any(t.startswith("pa_") for t in [X_type, y_type]) and not PYARROW_INSTALLED:
        pytest.skip("pyarrow is not installed")
2010

2011
    run_minimal_test(X_type=X_type, y_type=y_type, g_type="numpy", task=task, rng=rng)
2012
2013


2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
@pytest.mark.parametrize("X_type", all_x_types)
@pytest.mark.parametrize("y_type", all_y_types)
@pytest.mark.parametrize("g_type", all_group_types)
def test_ranking_minimally_works_with_all_accepted_data_types(
    X_type,
    y_type,
    g_type,
    rng,
):
    if any(t.startswith("pd_") for t in [X_type, y_type, g_type]) and not PANDAS_INSTALLED:
        pytest.skip("pandas is not installed")
    if any(t.startswith("pa_") for t in [X_type, y_type, g_type]) and not PYARROW_INSTALLED:
        pytest.skip("pyarrow is not installed")
2027

2028
    run_minimal_test(X_type=X_type, y_type=y_type, g_type=g_type, task="ranking", rng=rng)
2029
2030
2031
2032
2033
2034
2035
2036


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))
2037
    y_bin = (rng.random(size=nrows) <= 0.3).astype(np.float64)
2038
2039
2040
2041
2042
2043
2044
2045
    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"