`KNeighborsClassifier` with Default Label

### Describe the workflow you want to enable

In `KNeighborsClassifier`, for a user-provided callable `weights` parameter, it would be nice to allow for a situation with no winner. Say I want to give a weight of `0.0` to samples that are "too far away". Then, there can be a situation in which *all* neighbors get  a weight of `0.0`.

Currently:
- `predict_proba` would return a row of `0.0`, which is inconsistent, since it doesn't add up to `1.0`.
- `predict` would arbitrarily predict the first element of `classes_`, which is undesirable.

This is similar to the situation in `RadiusNeighborsClassifier`, when no samples are within the query radius. In `RadiusNeighborsClassifier` it is addressed by the `outlier_label` parameter, but in `KNeighborsClassifier` no such mechanism exists.

### Describe your proposed solution

Add a `default_label` parameter to `KNeighborsClassifier`, which would be the prediction in case all other classes get zero probability. Similarly to `outlier_label` in `RadiusNeighborsClassifier`, we can also support a `'most_frequent'` option and raise an exception when a label is not provided.

The basic logic could be implemented by making the following additions:
1. Add `default_label` to the possible `clsses_` by adding to `fit` (after `classes_` has been determined):
```python
if self.default_label is not None:
    if self.outputs_2d_:
        self.classes_ = [np.hstack((c, self.default_label)) for c in self.classes_ if self.default_label not in c]
    elif self.default_label not in self.classes_:
        self.classes_ = np.hstack((self.classes_, self.default_label))
```
2. Add to `predict_proba` (after `probas` has been determined):
```python
if self.default_label is not None:
    if self.outputs_2d_:
        for p, c in zip(probas, self.classes_):
            p[np.all(p == 0, axis=1), c == self.default_label] = 1.0
    else:
        probas[np.all(probas == 0, axis=1), self.classes_ == self.default_label] = 1.0
```
3. Add to `predict` a correction of the `mode`:
```python
for k, classes_k in enumerate(classes_):
    if weights is None:
        mode, _ = _mode(_y[neigh_ind, k], axis=1)
    else:
        mode, _ = weighted_mode(_y[neigh_ind, k], weights, axis=1)
        # Add the following two lines:
        if self.default_label is not None:
            mode[np.all(weights == 0, axis=1), 0] = np.where(classes_k == self.default_label)[0][0]

    mode = np.asarray(mode.ravel(), dtype=np.intp)
    y_pred[:, k] = classes_k.take(mode)
```


### Describe alternatives you've considered, if relevant

_No response_

### Additional context

_No response_

COMMENT:
I agree that the current behavior is problematic. But adding a new public parameter for such an edge case sounds too much.

So my suggestion would be to:
- do a PR to raise an informative error when this case happens (am not sure about the type of exception though) so that they can change there `weights` callable to avoid triggering this case in the first place;
- concurrently, to implement your specific logic, you can subclass scikit-learn `KNeighborsClassifier`'s `predict` and `predict_proba` to do what you want with limited extra maintenance on your side.

Labels: Bug, New Feature