Make more of the "tools" of scikit-learn Array API compatible

🚨 🚧 This issue requires a bit of patience and experience to contribute to 🚧 🚨 

- Original issue introducing array API in scikit-learn: #22352
- array API official doc/spec: https://data-apis.org/array-api/
- scikit-learn doc: https://scikit-learn.org/dev/modules/array_api.html

Please mention this issue when you create a PR, but please don't write "closes #26024" or "fixes #26024".

scikit-learn contains lots of useful tools, in addition to the many estimators it has. For example [metrics](https://scikit-learn.org/stable/modules/classes.html#sklearn-metrics-metrics), [pipelines](https://scikit-learn.org/stable/modules/classes.html#module-sklearn.pipeline), [pre-processing](https://scikit-learn.org/stable/modules/classes.html#module-sklearn.preprocessing) and [mode selection](https://scikit-learn.org/stable/modules/classes.html#module-sklearn.model_selection). These are useful to and used by people who do not necessarily use an estimator from scikit-learn. This is great.

The fact that many users install scikit-learn "just" to use `train_test_split` is a testament to how useful it is to provide easy to use tools that do the right(!) thing. Instead of everyone implementing them from scratch because it is "easy" and making mistakes along the way.

In this issue I'd like to collect and track work related to making it easier to use all these "tools" from scikit-learn even if you are not using Numpy arrays for your data. In particular thanks to the Array API standard it should be "not too much work" to make things usable with data that is in an array that conforms to the Array API standard.

There is work in #25956 and #22554 which adds the basic infrastructure needed to use "array API arrays".

The goal of this issue is to make code like the following work:
```python
>>> from sklearn.preprocessing import MinMaxScaler
>>> from sklearn import config_context
>>> from sklearn.datasets import make_classification
>>> import torch
>>> X_np, y_np = make_classification(random_state=0)
>>> X_torch = torch.asarray(X_np, device="cuda", dtype=torch.float32)
>>> y_torch = torch.asarray(y_np, device="cuda", dtype=torch.float32)

>>> with config_context(array_api_dispatch=True):
...     # For example using MinMaxScaler on PyTorch tensors
...     scale = MinMaxScaler()
...     X_trans = scale.fit_transform(X_torch, y_torch)
...     assert type(X_trans) == type(X_torch)
...     assert X_trans.device == X_torch.device
```

The first step (or maybe part of the first) is to check which of them already "just work". After that is done we can start the work (one PR per class/function) making changes.


## Guidelines for testing

General comment: most of the time when we add array API support to a function in scikit-learn, we do not touch the existing (numpy-only) tests to make sure that the PR does not change the default behavior of scikit-learn on traditional inputs when array API is not enabled.

In the case of an estimator, it can be enough to add the `array_api_support=True` estimator tag in a method named `__sklearn_tags__`. For metric functions, just register it in the `array_api_metric_checkers` in `sklearn/metrics/tests/test_common.py` to include it in the common test.

For other kinds of functions not covered by existing common tests, or when the array API support depends heavily on non-default values, it might be required to add one or more new test functions to the related module-level test file. The general testing scheme is the following:

- generate some random test data with numpy or `sklearn.datasets.make_*`;
- call the function once on the numpy inputs without enabling array API dispatch;
- convert the inputs to a namespace / device combo passed as parameter to the test;
- call the function with array API dispatching enabled (under a `with sklearn.config_context(array_api_dispatch=True)` block
- check that the results are on the same namespace and device as the input
- convert back the output to a numpy array using `_convert_to_numpy`
- compare the original / reference numpy results and the `xp` computation results converted back to numpy using `assert_allclose` or similar.

Those tests should have `array_api` somewhere in their name to makes sure that we can run all the array API compliance tests with a keyword search in the pytest command line, e.g.:

```
pytest -k array_api sklearn/some/subpackage
```

In particular, for cost reasons, our CUDA GPU CI only runs `pytest -k array_api sklearn`. So it's very important to respect this naming conventions, otherwise we will not tests all what we are supposed to test on CUDA.

More generally, look at merged array API pull requests to see how testing is typically handled.

COMMENT:
Mark an estimator or function as done if it not only "doesn't raise an exception" but also outputs a sensible value. The latter is something that will require a human at the start, but maybe later we can write a test for it.

Below a list of preprocessors and metrics. The lists are pretty long already, so I won't add more stuff until we make progress (or decide that a different area is a better starting point).

The next thing to work on is to work out if there is some generic advice around "fixing" these.

NOTE: it's possible to test the changes in your pull request on a CUDA GPU host for free with the help of this notebook on Google Colab: https://gist.github.com/EdAbati/ff3bdc06bafeb92452b3740686cc8d7c

---

Transformers from `sklearn.preprocessing`:
* [x] Binarizer https://github.com/scikit-learn/scikit-learn/pull/31190
* [ ] FunctionTransformer - no exception with pytorch `X`
* [ ] KBinsDiscretizer
* [x] KernelCenterer https://github.com/scikit-learn/scikit-learn/pull/27556
* [ ] LabelBinarizer #32582 (https://github.com/scikit-learn/scikit-learn/pull/28626)
* [x] LabelEncoder https://github.com/scikit-learn/scikit-learn/pull/27381
* [x] MaxAbsScaler https://github.com/scikit-learn/scikit-learn/pull/27110
* [x] MinMaxScaler https://github.com/scikit-learn/scikit-learn/pull/26243 and #29751
* [ ] ~~MultiLabelBinarizer~~ (_Not suitable for the array API_)
* [x] Normalizer https://github.com/scikit-learn/scikit-learn/pull/27558
* [ ] ~~OneHotEncoder~~ (_Mainly used to convert categorical data, thus not suitable for the array API_)
* [ ] ~~OrdinalEncoder~~ (_Mainly used to convert categorical data, thus not suitable for the array API_)
* [x] PolynomialFeatures https://github.com/scikit-learn/scikit-learn/pull/31580
* [ ] PowerTransformer (Requires array API support for `scipy.stats.yeojohnson` and `scipy.stats.boxcox`)
* [ ] QuantileTransformer (requires https://github.com/data-apis/array-api/issues/795)
* [ ] RobustScaler (requires https://github.com/data-apis/array-api/issues/795)
* [ ] SplineTransformer (requires https://github.com/data-apis/array-api/issues/795)
* [x] StandardScaler https://github.com/scikit-learn/scikit-learn/pull/27113
* [ ] TargetEncoder

<details>
<summary>Details</summary>
Code used to create the list:

```python
for name, Trn in discovery.all_estimators(type_filter="transformer"):
    if Trn.__module__.startswith("sklearn.preprocessing."):
        with config_context(array_api_dispatch=True):
            tr = Trn()
            try:
                tr.fit_transform(X_torch, y_torch)
                print(f"* [ ] {name} - no exception with pytorch X")
            except:
                print(f"* [ ] {name}")
```
</details>

Metrics from `sklearn.metrics`:
* [x] accuracy_score https://github.com/scikit-learn/scikit-learn/pull/27137
* [x] additive_chi2_kernel https://github.com/scikit-learn/scikit-learn/pull/29144
* [ ] adjusted_mutual_info_score - requires `contingency_matrix` and uses cython `expected_mutual_information`
* [ ] adjusted_rand_score - requires `contingency_matrix`
* [ ] auc https://github.com/scikit-learn/scikit-learn/pull/29755
* [ ] average_precision_score - requires `label_binarize`
* [x] balanced_accuracy_score - #32604
* [x] brier_score_loss https://github.com/scikit-learn/scikit-learn/pull/32422
* [ ] calinski_harabasz_score #32600 
* [x] check_paired_arrays #29014
* [x] check_pairwise_arrays #29014
* [ ] ~check_scoring~ n/a
* [x] chi2_kernel https://github.com/scikit-learn/scikit-learn/pull/29267
* [ ] class_likelihood_ratios - requires `confusion_matrix`
* [ ] classification_report
* [x] cohen_kappa_score - #32619
* [ ] completeness_score - requires `contingency_matrix`
* [x] confusion_matrix #30440, https://github.com/scikit-learn/scikit-learn/pull/30562
* [ ] consensus_score - uses `scipy.optimize.linear_sum_assignment` (not currently supported https://github.com/scipy/scipy/issues/18867)
* [ ] contingency_matrix #29251 (does main computation in numpy as we use `coo_matrix` for faster histogram calculation)
* [x] cosine_distances https://github.com/scikit-learn/scikit-learn/pull/29265
* [x] cosine_similarity https://github.com/scikit-learn/scikit-learn/pull/29014
* [ ] coverage_error - #32626 note uses `masked_array`
* [ ] d2_absolute_error_score https://github.com/scikit-learn/scikit-learn/pull/31671
* [ ] d2_pinball_score https://github.com/scikit-learn/scikit-learn/pull/31671
* [x] d2_tweedie_score https://github.com/scikit-learn/scikit-learn/pull/29207
* [ ] davies_bouldin_score
* [ ] dcg_score https://github.com/scikit-learn/scikit-learn/pull/29339, #31152
* [x] det_curve #32586
* [ ] ~distance_metrics~ n/a
* [x] entropy https://github.com/scikit-learn/scikit-learn/pull/29141
* [x] euclidean_distances https://github.com/scikit-learn/scikit-learn/pull/29433
* [x] explained_variance_score https://github.com/scikit-learn/scikit-learn/pull/29978
* [x] f1_score https://github.com/scikit-learn/scikit-learn/pull/27369
* [x] fbeta_score https://github.com/scikit-learn/scikit-learn/pull/30395
* [ ] fowlkes_mallows_score - requires `contingency_matrix`
* [ ] ~get_scorer~ n/a
* [ ] ~get_scorer_names~ n/a
* [x] hamming_loss https://github.com/scikit-learn/scikit-learn/pull/30838
* [ ] haversine_distances - uses cython `HaversineDistance`
* [ ] hinge_loss (_Depends on LabelBinarizer_: https://github.com/scikit-learn/scikit-learn/pull/28626)
* [ ] homogeneity_completeness_v_measure - requires `contingency_matrix`
* [ ] homogeneity_score - requires `contingency_matrix`
* [x] jaccard_score #31204 
* [ ] ~kernel_metrics~ n/a
* [ ] label_ranking_average_precision_score - uses sparse `csr_matrix` matrix
* [ ] label_ranking_loss
* [x] laplacian_kernel #32613 (https://github.com/scikit-learn/scikit-learn/pull/29881)
* [x] linear_kernel https://github.com/scikit-learn/scikit-learn/pull/29475
* [x] log_loss https://github.com/scikit-learn/scikit-learn/pull/32422
* [ ] ~make_scorer~ n/a
* [x] manhattan_distances https://github.com/scikit-learn/scikit-learn/pull/32597
* [ ] matthews_corrcoef - requires `confusion_matrix`
* [x] max_error  #29212
* [x] mean_absolute_error https://github.com/scikit-learn/scikit-learn/pull/27736
* [x] mean_absolute_percentage_error https://github.com/scikit-learn/scikit-learn/pull/29300
* [x] mean_gamma_deviance https://github.com/scikit-learn/scikit-learn/pull/29239
* [x] mean_pinball_loss https://github.com/scikit-learn/scikit-learn/pull/29978
* [x] mean_poisson_deviance https://github.com/scikit-learn/scikit-learn/pull/29227
* [x] mean_squared_error #29142
* [x] mean_squared_log_error https://github.com/scikit-learn/scikit-learn/pull/29709
* [x] mean_tweedie_deviance #28106
* [x] median_absolute_error #31406
* [x] multilabel_confusion_matrix #27369
* [ ] mutual_info_score - requires `contingency_matrix`
* [ ] nan_euclidean_distances
* [ ] ndcg_score #31152
* [ ] normalized_mutual_info_score - requires `contingency_matrix`
* [ ] pair_confusion_matrix - requires `contingency_matrix`
* [x] paired_cosine_distances https://github.com/scikit-learn/scikit-learn/pull/29112
* [ ] paired_distances
* [ ] paired_euclidean_distances
* [ ] paired_manhattan_distances
* [x] pairwise_distances https://github.com/scikit-learn/scikit-learn/pull/31658
* [ ] pairwise_distances_argmin
* [ ] pairwise_distances_argmin_min
* [ ] pairwise_distances_chunked - no exception with pytorch y
* [x] pairwise_kernels #29822
* [x] polynomial_kernel https://github.com/scikit-learn/scikit-learn/pull/29475
* [x] precision_recall_curve #32249
* [x] precision_recall_fscore_support https://github.com/scikit-learn/scikit-learn/pull/27369
* [x] precision_score https://github.com/scikit-learn/scikit-learn/pull/30395
* [x] r2_score #27904
* [ ] rand_score - requires `pair_confusion_matrix` (which requires `contingency_matrix`)
* [x] rbf_kernel https://github.com/scikit-learn/scikit-learn/pull/29433
* [x] recall_score https://github.com/scikit-learn/scikit-learn/pull/30395
* [ ] roc_auc_score - requires `label_binarize`
* [x] roc_curve #30878
* [x] sigmoid_kernel https://github.com/scikit-learn/scikit-learn/pull/29475
* [ ] silhouette_samples - requires `pairwise_distances_chunked`
* [ ] silhouette_score - uses numpy `RandomState.permutation`
* [ ] top_k_accuracy_score
* [ ] v_measure_score - requires `contingency_matrix`
* [x] zero_one_loss https://github.com/scikit-learn/scikit-learn/pull/27137

<details>
<summary>Details</summary>

```python
for name, func in discovery.all_functions():
    if func.__module__.startswith("sklearn.metrics."):
        with config_context(array_api_dispatch=True):
            try:
                func(y_torch, y_torch)
                print(f"* [ ] {name} - no exception with pytorch y")
            except:
                print(f"* [ ] {name}")
```

</details>

Labels: API, Meta-issue, Array API