Repository: scikit-learn/scikit-learn
File: sklearn/metrics/_ranking.py (Python)

Code before:
    --------
    >>> import numpy as np
    >>> from sklearn.metrics import average_precision_score
    >>> y_true = np.array([0, 0, 1, 1])
    >>> y_scores = np.array([0.1, 0.4, 0.35, 0.8])
    >>> average_precision_score(y_true, y_scores)
    0.83
    >>> y_true = np.array([0, 0, 1, 1, 2, 2])
    >>> y_scores = np.array([
    ...     [0.7, 0.2, 0.1],
    ...     [0.4, 0.3, 0.3],
    ...     [0.1, 0.8, 0.1],
    ...     [0.2, 0.3, 0.5],
    ...     [0.4, 0.4, 0.2],
    ...     [0.1, 0.2, 0.7],
    ... ])
    >>> average_precision_score(y_true, y_scores)
    0.77
    """
    xp, _ = get_namespace(y_true, y_score, sample_weight)

    if not get_config().get("array_api_dispatch", False):
        y_true = _convert_to_numpy(y_true, xp=xp)
        y_score = _convert_to_numpy(y_score, xp=xp)
        if sample_weight is not None:
            sample_weight = _convert_to_numpy(sample_weight, xp=xp)

    def _binary_uninterpolated_average_precision(
        y_true,
        y_score,
        pos_label=1,
        sample_weight=None,
        xp=xp,
    ):
        precision, recall, _ = precision_recall_curve(
            y_true,
            y_score,
            pos_label=pos_label,
            sample_weight=sample_weight,
        )
        # Return the step function integral
        # The following works because the last entry of precision is
        # guaranteed to be 1, as returned by precision_recall_curve.
        # Due to numerical error, we can get `-0.0` and we therefore clip it.
        return float(max(0.0, -xp.sum(xp.diff(recall) * precision[:-1])))

    y_type = type_of_target(y_true, input_name="y_true")
    present_labels = xp.unique_values(y_true)

    if y_type == "binary":
        if present_labels.shape[0] == 2 and pos_label not in present_labels:

Review comment (bug):
This (hopefully) fixes an error occurring on the CI ([link to output)](https://dev.azure.com/scikit-learn/scikit-learn/_build/results?buildId=83504&view=logs&j=dde5042c-7464-5d47-9507-31bdd2ee0a3a&t=4bd2dad8-62b3-5bf9-08a5-a9880c530c94):

```
E TypeError: sum() received an invalid combination of arguments - got (out=NoneType, axis=int, ), but expected one of:
E * (*, torch.dtype dtype = None)
E  didn't match because some of the keywords were incorrect: out, axis
E  * (tuple of ints dim, bool keepdim = False, *, torch.dtype dtype = None)
E  * (tuple of names dim, bool keepdim = False, *, torch.dtype dtype = None)
```

@lesteve and I investigated this together. It seems that `numpy.sum() `when passed a torch tensor, internally tries to dispatch to torch, but that fails because `torch.sum() `doesn't have the `out` argument. The same happens with `np.repeat()` which has an `axis` argument, but `torch.repeat()` hasn't (which had raised in my setup locally).

Code after:
    >>> import numpy as np
    >>> from sklearn.metrics import average_precision_score
    >>> y_true = np.array([0, 0, 1, 1])
    >>> y_scores = np.array([0.1, 0.4, 0.35, 0.8])
    >>> average_precision_score(y_true, y_scores)
    0.83
    >>> y_true = np.array([0, 0, 1, 1, 2, 2])
    >>> y_scores = np.array([
    ...     [0.7, 0.2, 0.1],
    ...     [0.4, 0.3, 0.3],
    ...     [0.1, 0.8, 0.1],
    ...     [0.2, 0.3, 0.5],
    ...     [0.4, 0.4, 0.2],
    ...     [0.1, 0.2, 0.7],
    ... ])
    >>> average_precision_score(y_true, y_scores)
    0.77
    """
    xp, _, device = get_namespace_and_device(y_score)
    y_true, sample_weight = move_to(y_true, sample_weight, xp=xp, device=device)

    if sample_weight is not None:
        sample_weight = column_or_1d(sample_weight)

    def _binary_uninterpolated_average_precision(
        y_true,
        y_score,
        pos_label=1,
        sample_weight=None,
        xp=xp,
    ):
        precision, recall, _ = precision_recall_curve(
            y_true,
            y_score,
            pos_label=pos_label,
            sample_weight=sample_weight,
        )
        # Return the step function integral
        # The following works because the last entry of precision is
        # guaranteed to be 1, as returned by precision_recall_curve.
        # Due to numerical error, we can get `-0.0` and we therefore clip it.
        return float(max(0.0, -xp.sum(xp.diff(recall) * precision[:-1])))

    y_type = type_of_target(y_true, input_name="y_true")
    present_labels = xp.unique_values(y_true)

    if y_type == "binary":
        if present_labels.shape[0] == 2 and pos_label not in present_labels:
            raise ValueError(
                f"pos_label={pos_label} is not a valid label. It should be "
                f"one of {present_labels}"