Speed up classification_report

### Describe the workflow you want to enable

I'm concerned with slow execution speed of the classification_report  procedure which makes it barely suitable for production-grade workloads.
On a 8M sample it already takes 15 seconds whereas simple POC numba implementation takes 20 to 40 **milli**seconds. I understand that sklearn code is well covered with tests, has wide functionality, follows style guildlines and best practices, but there should be no excuse for the performace leap with simple POC of magnitude that big (x1000).

```python
import numpy as np
from sklearn.metrics import classification_report

y_true = np.random.randint(0, 2, size=2 ** 23)
y_pred = y_true.copy()
np.random.shuffle(y_pred[2 ** 20 : 2 ** 21])

print(classification_report(y_true=y_true, y_pred=y_pred, digits=10, output_dict=False, zero_division=0,))
```

>               precision    recall  f1-score   support
> 
>            0  0.9373906697 0.9373906697 0.9373906697   4192570
>            1  0.9374424159 0.9374424159 0.9374424159   4196038
> 
>     accuracy                      0.9374165535   8388608
>     macro avg  0.9374165428 0.9374165428 0.9374165428   8388608
>     weighted avg  0.9374165535 0.9374165535 0.9374165535   8388608
> 
> time: 18.6 s (started: 2023-07-08 16:10:08 +03:00)
> 


`print(own_classification_report(y_true=y_true, y_pred=y_pred, zero_division=0))`


> (3930076, 3933544, 262494, 262494, 0.9374165534973145, array([4192570, 4196038], dtype=int64), array([0.93739067, 0.93744242]), array([0.93739067, 0.93744242]), array([0.93739067, 0.93744242]))
> time: 16 ms (started: 2023-07-08 16:11:18 +03:00)

### Describe your proposed solution

```python
import numpy as np
from numba import njit


@njit()
def own_classification_report(y_true: np.ndarray, y_pred: np.ndarray, nclasses: int = 2, zero_division: int = 0):

    correct = np.zeros(nclasses, dtype=np.int64)
    wrong = np.zeros(nclasses, dtype=np.int64)
    for truth, pred in zip(y_true, y_pred):
        if pred == truth:
            correct[pred] += 1
        else:
            wrong[pred] += 1
    tn, tp = correct
    fn, fp = wrong
    accuracy = (tn + tp) / len(y_true)

    groups = np.array([(tn + fn), (tp + fp)])
    supports = np.array([(tn + fp), (tp + fn)])

    precisions = np.array([tn, tp]) / groups
    recalls = np.array([tn, tp]) / supports
    f1s = 2 * (precisions * recalls) / (precisions + recalls)

    for arr in (precisions, recalls, f1s):
        np.nan_to_num(arr, copy=False, nan=zero_division)

    return tn, tp, fn, fp, accuracy, supports, precisions, recalls, f1s
```

### Describe alternatives you've considered, if relevant

Can someone from original classification_report authors take a look please why it's taking so long?

### Additional context

I have the latest versions of scikit-learn and numba to this date, an Intel CPU with AVX.

COMMENT:
Sure, I can use my custom code for now, just wanted the community to also benefit from my findings.

I looked at the source codes in more details and here's the ballpark outline of the time spent in classification_report:

1. **params checking**: 1.58 sec ( _check_targets(y_true, y_pred) +unique_labels(y_true, y_pred))

2. calling **precision_recall_fscore_support** with average=None: 3.78 sec

3. then calling the same **precision_recall_fscore_support but for each of ('micro', 'macro', 'weighted') averages** kind, which involves repeating all inner checks of precision_recall_fscore_support and computing the same multilabel_confusion_matrix again and again, that amounts to 11.3 seconds.


Can we please go with a very simple fix for start: make MCM an optional input and output parameter to the precision_recall_fscore_support  function, and if it's provided as an input, naturally skip expensive steps of
`labels = _check_set_wise_labels(y_true, y_pred, average, labels, pos_label)` (1.56 sec)
and
`MCM = multilabel_confusion_matrix...` (2.27 sec)
each of the 3 additional times?

I guess it will already take away a lion's share of repetitive computing, if my math is correct it will reduce the report building time from 18 to like 5 seconds.

To address @glemaitre 's point, I indeed met the case when the model's training time is comparable to performance estimation time. It happened because I am using additional group-wise estimation, that is, I compute metrics not only for the whole set but also for 30 of its subgroups separately. I literally stuck at situation where quality estimation takes up to 5 minutes, and all that time the (quite costly) Dask cluster has to wait idle. Multiply it by the number of models I want to train (hundreds), and it becomes a substantial concern cost-wise.

Imho it's very important to have fast metrics implementations in sklearn 'cause it's a de facto gold standard in modern ML, and many 3rd party libs rely on it, for example, Catboost. Not only classification_report can be sped up, I also just had good results with roc_auc (x8 speedup over sklearn with numba and numpy). 

Why it might be important, again, is for example 'cause Catboost does not yet have roc_auc GPU implementation and has to rely on CPU implementation (from sklearn, it seems, as their roc_auc timings are very comparable), and training implicitly often includes computing metrics regularly (it would be not fair to only count metrics evaluation as a post-training process, 'cause **early stopping** is used very often and it requires computing metrics regularly, i.e., hundreds and thousands of times per training cycle). On a big dataset that becomes a significant overhead if not implemented efficiently.

Labels: Performance, Needs Investigation