diff --git a/.ci/test-r-package.sh b/.ci/test-r-package.sh index ef6ffe591351..24d3991d8504 100755 --- a/.ci/test-r-package.sh +++ b/.ci/test-r-package.sh @@ -49,6 +49,7 @@ if [[ $OS_NAME == "linux" ]]; then --no-install-recommends \ -y \ devscripts \ + libuv1-dev \ r-base-core=${R_LINUX_VERSION} \ r-base-dev=${R_LINUX_VERSION} \ texinfo \ diff --git a/.github/workflows/r_package.yml b/.github/workflows/r_package.yml index 6d65df268105..5f2b6ad8930d 100644 --- a/.github/workflows/r_package.yml +++ b/.github/workflows/r_package.yml @@ -273,15 +273,16 @@ jobs: # references: # * CRAN "additional checks": https://cran.r-project.org/web/checks/check_issue_kinds.html # * images: https://r-hub.github.io/containers/containers.html + # + # Generally, only images that are still supported, listed at + # https://r-hub.github.io/containers/#available-containers, should be used. image: - - clang16 - - clang17 - - clang18 - - clang19 - - clang20 - - gcc14 - - intel + - clang21 + - clang22 + - gcc16 - rchk + - ubuntu-clang + - ubuntu-gcc12 runs-on: ubuntu-latest container: ghcr.io/r-hub/containers/${{ matrix.image }}:latest steps: @@ -302,6 +303,7 @@ jobs: apt-get install --no-install-recommends -y \ cmake \ devscripts \ + libcurl4-openssl-dev \ libuv1-dev \ texinfo \ texlive-latex-extra \ @@ -315,10 +317,15 @@ jobs: yum install -y \ cmake \ devscripts \ + libcurl-devel \ libuv-devel \ qpdf \ texinfo \ texinfo-tex \ + texlive-amsmath \ + texlive-amsfonts \ + texlive-collection-fontsrecommended \ + texlive-collection-latexrecommended \ texlive-latex \ tidy fi @@ -348,8 +355,16 @@ jobs: fi fi - # 'testthat' is not needed by 'rchk', so avoid installing it until here - Rscript -e "install.packages('testthat', repos = 'https://cran.rstudio.com', Ncpus = parallel::detectCores())" + # why these packages? + # + # - 'curl' is not a dependency of {lightgbm}, but it's needed for 'R CMD check' URL checks + # - 'testthat' is not needed by 'rchk', so avoid installing it until here + # + Rscript -e " + install.packages(c('curl', 'testthat'), + repos = 'https://cran.rstudio.com', + Ncpus = parallel::detectCores()) + " if [[ "${{ matrix.image }}" =~ "clang" ]]; then # allowing the following NOTEs (produced by default in the clang images): diff --git a/R-package/tests/testthat/test_basic.R b/R-package/tests/testthat/test_basic.R index 4029b7bdb0b2..a18839397822 100644 --- a/R-package/tests/testthat/test_basic.R +++ b/R-package/tests/testthat/test_basic.R @@ -765,6 +765,52 @@ test_that("lgb.train() raises an informative error for unrecognized objectives", }, regexp = "Unknown objective type name: not_a_real_objective") }) +test_that("lgb.train() raises an informative error for unrecognized metrics", { + dtrain <- lgb.Dataset( + data = train$data + , label = train$label + , params = list(num_threads = .LGB_MAX_THREADS) + ) + expect_error({ + capture.output({ + bst <- lgb.train( + data = dtrain + , valids = list(valid = dtrain) + , params = list( + objective = "binary" + , metric = "nonsense" + , verbosity = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + , num_iterations = 2L + ) + ) + }, type = "message") + }, regexp = "Unknown metric 'nonsense'") +}) + +test_that("lgb.cv() raises an informative error for unrecognized metrics", { + dtrain <- lgb.Dataset( + data = train$data + , label = train$label + , params = list(num_threads = .LGB_MAX_THREADS) + ) + expect_error({ + capture.output({ + bst <- lgb.cv( + data = dtrain + , params = list( + objective = "binary" + , metric = "nonsense" + , verbosity = .LGB_VERBOSITY + , num_threads = .LGB_MAX_THREADS + ) + , nfold = 2L + , nrounds = 2L + ) + }, type = "message") + }, regexp = "Unknown metric 'nonsense'") +}) + test_that("lgb.train() respects parameter aliases for objective", { nrounds <- 3L dtrain <- lgb.Dataset( diff --git a/python-package/lightgbm/basic.py b/python-package/lightgbm/basic.py index e1d32b3002cd..3f63e14182c1 100644 --- a/python-package/lightgbm/basic.py +++ b/python-package/lightgbm/basic.py @@ -3115,7 +3115,8 @@ def set_weight( # Check if the weight contains values other than one if weight is not None: if _is_pyarrow_array(weight): - if pa_compute.all(pa_compute.equal(weight, 1)).as_py(): + # TODO: remove 'type: ignore[attr-defined]' when https://github.com/apache/arrow/issues/49831 is resolved. + if pa_compute.all(pa_compute.equal(weight, 1)).as_py(): # type: ignore[attr-defined] weight = None elif np.all(weight == 1): weight = None diff --git a/src/metric/metric.cpp b/src/metric/metric.cpp index 1b1857602d9b..ac022c87d270 100644 --- a/src/metric/metric.cpp +++ b/src/metric/metric.cpp @@ -136,6 +136,12 @@ Metric* Metric::CreateMetric(const std::string& type, const Config& config) { #ifdef USE_CUDA } #endif // USE_CUDA + // "custom" is a sentinel value produced by ParseMetricAlias for "none" / "na" / "null" / "custom"; + // it means the user deliberately requested no built-in metric, so returning nullptr is intentional. + // Any other unrecognised name is a user mistake and deserves a clear error. + if (type != std::string("custom")) { + Log::Fatal("Unknown metric '%s'. To avoid enabling LightGBM built-in metrics, pass metric='none'. Otherwise, ensure 'metric' contains only supported values.", type.c_str()); + } return nullptr; } diff --git a/tests/python_package_test/test_engine.py b/tests/python_package_test/test_engine.py index 7db16c7ff176..cdbbd6287e2d 100644 --- a/tests/python_package_test/test_engine.py +++ b/tests/python_package_test/test_engine.py @@ -4811,6 +4811,42 @@ def test_train_raises_informative_error_for_params_of_wrong_type(): lgb.train(params, dtrain) +def test_train_raises_informative_error_for_unknown_metric(): + X, y = load_breast_cancer(return_X_y=True) + dtrain = lgb.Dataset(X, label=y) + params = {"objective": "binary", "metric": "nonsense", "verbose": -1} + with pytest.raises(lgb.basic.LightGBMError, match="Unknown metric 'nonsense'"): + lgb.train(params, dtrain, num_boost_round=1, valid_sets=[dtrain]) + + +@pytest.mark.parametrize( + "metric", + [ + "none", + ["none", "null"], + ["None"], + ["NULL"], + ], +) +def test_train_sentinel_metric_disables_builtin_metrics(metric): + X, y = load_breast_cancer(return_X_y=True) + dtrain = lgb.Dataset(X, label=y) + evals_result = {} + params = {"objective": "binary", "metric": metric, "verbose": -1} + lgb.train(params, dtrain, num_boost_round=1, valid_sets=[dtrain], callbacks=[lgb.record_evaluation(evals_result)]) + assert evals_result == {} + + +def test_train_with_metric_python_none_uses_default_metric(): + X, y = load_breast_cancer(return_X_y=True) + dtrain = lgb.Dataset(X, label=y) + dvalid = lgb.Dataset(X, label=y, reference=dtrain) + evals_result = {} + params = {"objective": "binary", "metric": None, "verbose": -1} + lgb.train(params, dtrain, num_boost_round=1, valid_sets=[dvalid], callbacks=[lgb.record_evaluation(evals_result)]) + assert "binary_logloss" in evals_result["valid_0"] + + def test_quantized_training(): X, y = make_synthetic_regression() ds = lgb.Dataset(X, label=y) diff --git a/tests/python_package_test/test_sklearn.py b/tests/python_package_test/test_sklearn.py index b312b6f8d1c2..2834a779a49e 100644 --- a/tests/python_package_test/test_sklearn.py +++ b/tests/python_package_test/test_sklearn.py @@ -52,6 +52,7 @@ SKLEARN_MAJOR, SKLEARN_MINOR, *_ = _sklearn_version.split(".") SKLEARN_VERSION_GTE_1_6 = (int(SKLEARN_MAJOR), int(SKLEARN_MINOR)) >= (1, 6) +SKLEARN_VERSION_GTE_1_7 = (int(SKLEARN_MAJOR), int(SKLEARN_MINOR)) >= (1, 7) decreasing_generator = itertools.count(0, -1) estimator_classes = (lgb.LGBMModel, lgb.LGBMClassifier, lgb.LGBMRegressor, lgb.LGBMRanker) @@ -453,7 +454,15 @@ def test_classifier_chain(): X, y = make_multilabel_classification(n_samples=100, n_features=20, n_classes=n_outputs, random_state=0) X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, random_state=42) order = [2, 0, 1] - clf = ClassifierChain(base_estimator=lgb.LGBMClassifier(n_estimators=10), order=order, random_state=42) + # 'base_estimator' parameter was deprecated in scikit-learn 1.7 and removed in 1.9 + # + # * https://github.com/scikit-learn/scikit-learn/pull/30152 + # * https://github.com/scikit-learn/scikit-learn/pull/33750 + # + if SKLEARN_VERSION_GTE_1_7: + clf = ClassifierChain(estimator=lgb.LGBMClassifier(n_estimators=10), order=order, random_state=42) + else: + clf = ClassifierChain(base_estimator=lgb.LGBMClassifier(n_estimators=10), order=order, random_state=42) clf.fit(X_train, y_train) score = clf.score(X_test, y_test) assert score >= 0.2 @@ -470,7 +479,15 @@ def test_regressor_chain(): X, y = bunch["data"], bunch["target"] X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, random_state=42) order = [2, 0, 1] - reg = RegressorChain(base_estimator=lgb.LGBMRegressor(n_estimators=10), order=order, random_state=42) + # 'base_estimator' parameter was deprecated in scikit-learn 1.7 and removed in 1.9 + # + # * https://github.com/scikit-learn/scikit-learn/pull/30152 + # * https://github.com/scikit-learn/scikit-learn/pull/33750 + # + if SKLEARN_VERSION_GTE_1_7: + reg = RegressorChain(estimator=lgb.LGBMRegressor(n_estimators=10), order=order, random_state=42) + else: + reg = RegressorChain(base_estimator=lgb.LGBMRegressor(n_estimators=10), order=order, random_state=42) reg.fit(X_train, y_train) y_pred = reg.predict(X_test) _, score, _ = mse(y_test, y_pred)