Skip to content

Commit 44cd390

Browse files
committed
fix: handle sklearn deprecations
- `needs_proba` and `needs_threshold` arguments for `make_scorer()` are deprecated in favor of `response_method`. - `LinearSVC` and `LinearSVR` now need to have `dual` explicitly set to "auto". - `AdaBoostClassifier` now needs to have algorithm` set to "SAMME". - Update custom metrics documentation. - Update tests and test data.
1 parent 6ca87a2 commit 44cd390

File tree

7 files changed

+38
-23
lines changed

7 files changed

+38
-23
lines changed

doc/custom_metrics.rst

+16-5
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,27 @@ Writing Custom Metric Functions
1212

1313
First, let's look at how to write valid custom metric functions. A valid custom metric function
1414
must take two array-like positional arguments: the first being the true labels or scores, and the
15-
second being the predicted labels or scores. This function can also take three optional keyword arguments:
15+
second being the predicted labels or scores. This function can also take two optional keyword arguments:
1616

1717
1. ``greater_is_better``: a boolean keyword argument that indicates whether a higher value of the metric indicates better performance (``True``) or vice versa (``False``). The default value is ``True``.
18-
2. ``needs_proba``: a boolean keyword argument that indicates whether the metric function requires probability estimates. The default value is ``False``.
19-
3. ``needs_threshold``: a boolean keyword argument that indicates whether the metric function takes a continuous decision certainty. The default value is ``False``.
18+
19+
2. ``response_method`` : a string keyword argument that specifies the response method to use to get predictions from an estimator. Possible values are:
20+
21+
- ``"predict"`` : uses estimator's `predict() <https://scikit-learn.org/stable/glossary.html#term-predict>`__ method to get class labels
22+
- ``"predict_proba"`` : uses estimator's `predict_proba() <https://scikit-learn.org/stable/glossary.html#term-predict_proba>`__ method to get class probabilities
23+
- ``"decision_function"`` : uses estimator's `decision_function() <https://scikit-learn.org/stable/glossary.html#term-decision_function>`__ method to get continuous decision function values
24+
- If the value is a list or tuple of the above strings, it indicates that the scorer should use the first method in the list which is implemented by the estimator.
25+
- If the value is ``None``, it is the same as ``"predict"``.
26+
27+
The default value for ``response_method`` is ``None``.
2028

2129
Note that these keyword arguments are identical to the keyword arguments for the `sklearn.metrics.make_scorer() <https://scikit-learn.org/stable/modules/generated/sklearn.metrics.make_scorer.html#sklearn.metrics.make_scorer>`_ function and serve the same purpose.
2230

23-
In short, custom metric functions take two required positional arguments (order matters) and three optional keyword arguments. Here's a simple example of a custom metric function: F\ :sub:`β` with β=0.75 defined in a file called ``custom.py``.
31+
.. important::
32+
33+
Previous versions of SKLL offered the ``needs_proba`` and ``needs_threshold`` keyword arguments for custom metrics but these are now deprecated in scikit-learn and replaced by the ``response_method`` keyword argument. To replicate the behavior of ``needs_proba=True``, use ``response_method="predict_proba"`` instead and to replicate ``needs_threshold=True``, use ``response_method=("decision_function", "predict_proba")`` instead.
34+
35+
In short, custom metric functions take two required positional arguments (order matters) and two optional keyword arguments. Here's a simple example of a custom metric function: F\ :sub:`β` with β=0.75 defined in a file called ``custom.py``.
2436

2537
.. code-block:: python
2638
:caption: custom.py
@@ -30,7 +42,6 @@ In short, custom metric functions take two required positional arguments (order
3042
def f075(y_true, y_pred):
3143
return fbeta_score(y_true, y_pred, beta=0.75)
3244
33-
3445
Obviously, you may write much more complex functions that aren't directly
3546
available in scikit-learn. Once you have written your metric function, the next
3647
step is to use it in your SKLL experiment.

skll/learner/__init__.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -252,10 +252,12 @@ def __init__(
252252
"produce probabilities, results will not be exactly "
253253
"replicable when using SVC and probability mode."
254254
)
255+
elif issubclass(self._model_type, AdaBoostClassifier):
256+
self._model_kwargs["algorithm"] = "SAMME"
257+
self._model_kwargs["n_estimators"] = 500
255258
elif issubclass(
256259
self._model_type,
257260
(
258-
AdaBoostClassifier,
259261
AdaBoostRegressor,
260262
BaggingClassifier,
261263
BaggingRegressor,
@@ -268,6 +270,8 @@ def __init__(
268270
self._model_kwargs["n_estimators"] = 500
269271
elif issubclass(self._model_type, DummyClassifier):
270272
self._model_kwargs["strategy"] = "prior"
273+
elif issubclass(self._model_type, (LinearSVC, LinearSVR)):
274+
self._model_kwargs["dual"] = "auto"
271275
elif issubclass(self._model_type, SVR):
272276
self._model_kwargs["cache_size"] = 1000
273277
self._model_kwargs["gamma"] = "scale"
@@ -950,7 +954,7 @@ def train(
950954
metrics_module = import_module("skll.metrics")
951955
metric_func = getattr(metrics_module, "correlation")
952956
_CUSTOM_METRICS[new_grid_objective] = make_scorer(
953-
metric_func, corr_type=grid_objective, needs_proba=True
957+
metric_func, corr_type=grid_objective, response_method="predict_proba"
954958
)
955959
grid_objective = new_grid_objective
956960

skll/metrics.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,7 @@ def register_custom_metric(custom_metric_path: PathOrStr, custom_metric_name: st
298298
# extract any "special" keyword arguments from the metric function
299299
metric_func_parameters = signature(metric_func).parameters
300300
make_scorer_kwargs = {}
301-
for make_scorer_kwarg in ["greater_is_better", "needs_proba", "needs_threshold"]:
301+
for make_scorer_kwarg in ["greater_is_better", "response_method"]:
302302
if make_scorer_kwarg in metric_func_parameters:
303303
parameter = metric_func_parameters.get(make_scorer_kwarg)
304304
if parameter is not None:

tests/configs/test_send_warnings_to_log.template.cfg

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ task=cross_validate
44

55
[Input]
66
featuresets=[["test_send_warnings_to_log"]]
7-
learners=["LinearSVC"]
7+
learners=["DummyClassifier"]
88
suffix=.jsonlines
99
num_cv_folds=2
1010

tests/other/custom_metrics.py

+8-7
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
"""Custom metrics for testing purposes."""
12
from sklearn.metrics import (
23
average_precision_score,
34
f1_score,
@@ -8,31 +9,31 @@
89
)
910

1011

11-
def f075_macro(y_true, y_pred):
12+
def f075_macro(y_true, y_pred): # noqa: D103
1213
return fbeta_score(y_true, y_pred, beta=0.75, average="macro")
1314

1415

15-
def ratio_of_ones(y_true, y_pred):
16+
def ratio_of_ones(y_true, y_pred): # noqa: D103
1617
true_ones = [label for label in y_true if label == 1]
1718
pred_ones = [label for label in y_pred if label == 1]
1819
return len(pred_ones) / (len(true_ones) + len(pred_ones))
1920

2021

21-
def r2(y_true, y_pred):
22+
def r2(y_true, y_pred): # noqa: D103
2223
return r2_score(y_true, y_pred)
2324

2425

25-
def one_minus_precision(y_true, y_pred, greater_is_better=False):
26+
def one_minus_precision(y_true, y_pred, greater_is_better=False): # noqa: D103
2627
return 1 - precision_score(y_true, y_pred, average="binary")
2728

2829

29-
def one_minus_f1_macro(y_true, y_pred, greater_is_better=False):
30+
def one_minus_f1_macro(y_true, y_pred, greater_is_better=False): # noqa: D103
3031
return 1 - f1_score(y_true, y_pred, average="macro")
3132

3233

33-
def fake_prob_metric(y_true, y_pred, needs_proba=True):
34+
def fake_prob_metric(y_true, y_pred, response_method="predict_proba"): # noqa: D103
3435
return average_precision_score(y_true, y_pred)
3536

3637

37-
def fake_prob_metric_multiclass(y_true, y_pred, needs_proba=True):
38+
def fake_prob_metric_multiclass(y_true, y_pred, response_method="predict_proba"): # noqa: D103
3839
return roc_auc_score(y_true, y_pred, average="macro", multi_class="ovo")

tests/test_classification.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -542,7 +542,7 @@ def test_sparse_predict(self): # noqa: D103
542542
(0.45, 0.52),
543543
(0.52, 0.5),
544544
(0.48, 0.5),
545-
(0.49, 0.5),
545+
(0.5, 0.5),
546546
(0.54, 0.5),
547547
(0.43, 0),
548548
(0.53, 0.57),
@@ -814,8 +814,8 @@ def check_adaboost_predict(self, base_estimator, algorithm, expected_score):
814814
def test_adaboost_predict(self): # noqa: D103
815815
for base_estimator_name, algorithm, expected_score in zip(
816816
["MultinomialNB", "DecisionTreeClassifier", "SGDClassifier", "SVC"],
817-
["SAMME.R", "SAMME.R", "SAMME", "SAMME"],
818-
[0.46, 0.52, 0.46, 0.5],
817+
["SAMME", "SAMME", "SAMME", "SAMME"],
818+
[0.49, 0.52, 0.46, 0.5],
819819
):
820820
yield self.check_adaboost_predict, base_estimator_name, algorithm, expected_score
821821

tests/test_output.py

+3-4
Original file line numberDiff line numberDiff line change
@@ -1208,12 +1208,11 @@ def test_send_warnings_to_log(self):
12081208
# Check experiment log output
12091209
# The experiment log file should contain warnings related
12101210
# to the use of sklearn
1211-
with open(output_dir / "test_send_warnings_to_log_LinearSVC.log") as f:
1211+
with open(output_dir / "test_send_warnings_to_log_DummyClassifier.log") as f:
12121212
log_content = f.read()
12131213
convergence_sklearn_warning_re = re.compile(
1214-
r"WARNING - [^\n]+sklearn.svm._base\.py:\d+: ConvergenceWarning:"
1215-
r"Liblinear failed to converge, increase the number of iterations"
1216-
r"\."
1214+
r"WARNING - [^\n]+sklearn.metrics._classification\.py:\d+: "
1215+
r"UndefinedMetricWarning:Precision is ill-defined and being set to 0.0"
12171216
)
12181217
assert convergence_sklearn_warning_re.search(log_content) is not None
12191218

0 commit comments

Comments
 (0)