Wrong Mutual Information Calculation

### Describe the bug

#### Issue
I encountered a bug unexpectedly while reviewing some metrics in a project.
When calculating mutual information using the `mutual_info_classif`, I noticed values higher than entropy, which is [impossible](https://en.wikipedia.org/wiki/Mutual_information#/media/File:Figchannel2017ab.svg). There is no such issue with `mutual_info_regression` (although, there, the self-mi is far from entropy, which may be another interesting case).

##### Implication
Any algorithm sorting features based on `mutual_info_classif` or any metric based on this function may be affected.

Thanks a lot for putting time on this.


P.S. In the minimal example, the feature is fixed (all one). However, I encountered the same issue in other scenarios as well. The example is just more simplified. The problem persists on both Linux and Mac. I attached personal computer session info.

### Steps/Code to Reproduce

```python
import numpy as np
import pandas as pd

from sklearn.feature_selection import mutual_info_classif

big_n = 1_000_000
bug_df = pd.DataFrame({
    'feature': np.ones(big_n),
    'target': (np.arange(big_n) < 100).astype(int),
})
bug_df

mi = mutual_info_classif(bug_df[['feature']], bug_df['target'])
entropy = mutual_info_classif(bug_df[['target']], bug_df['target'])

print(f"mi: {mi[0] :.6f}")
print(f"self-mi (entropy): {entropy[0] :.6f}")

from scipy import stats

scipy_entropy = stats.entropy([bug_df['target'].mean(), 1 - bug_df['target'].mean()])

print(f"scipy entropy: {scipy_entropy :.6f}")
```

### Expected Results

```
mi: 0.000000
self-mi (entropy): 0.001023
scipy entropy: 0.001021
```

### Actual Results

```
mi: 0.215495
self-mi (entropy): 0.001023
scipy entropy: 0.001021
```

### Versions

```shell
System:
    python: 3.10.13 (main, Sep 11 2023, 08:16:02) [Clang 14.0.6 ]
executable: /Users/*/miniconda3/envs/*/bin/python
   machine: macOS-15.1.1-arm64-arm-64bit

Python dependencies:
      sklearn: 1.6.1
          pip: 23.3.1
   setuptools: 68.2.2
        numpy: 1.26.1
        scipy: 1.15.1
       Cython: None
       pandas: 2.2.3
   matplotlib: 3.8.2
       joblib: 1.3.2
threadpoolctl: 3.2.0

Built with OpenMP: True

threadpoolctl info:
       user_api: blas
   internal_api: openblas
    num_threads: 8
         prefix: libopenblas
       filepath: /Users/*/miniconda3/envs/*/lib/python3.10/site-packages/numpy/.dylibs/libopenblas64_.0.dylib
        version: 0.3.23.dev
threading_layer: pthreads
   architecture: armv8

       user_api: openmp
   internal_api: openmp
    num_threads: 8
         prefix: libomp
       filepath: /Users/*/miniconda3/envs/*/lib/python3.10/site-packages/sklearn/.dylibs/libomp.dylib
        version: None
```

COMMENT:
More experiments with the constant feature:

```python
>>> from sklearn.feature_selection._mutual_info import _estimate_mi
>>> bug_df
        feature  target
0           1.0       1
1           1.0       1
2           1.0       1
3           1.0       1
4           1.0       1
...         ...     ...
999995      1.0       0
999996      1.0       0
999997      1.0       0
999998      1.0       0
999999      1.0       0

[1000000 rows x 2 columns]
>>> _estimate_mi(bug_df[["feature"]].values, bug_df["target"], discrete_target=True, discrete_features=True)
array([0.])
>>> _estimate_mi(bug_df[["feature"]].values, bug_df["target"], discrete_target=True, discrete_features=False, n_neighbors=3)
array([0.2142434])
>>> _estimate_mi(bug_df[["feature"]].values, bug_df["target"], discrete_target=True, discrete_features=False, n_neighbors=10)
array([0.05531741])
>>> _estimate_mi(bug_df[["feature"]].values, bug_df["target"], discrete_target=True, discrete_features=False, n_neighbors=30)
array([0.01637024])
```

and here is with the near constant feature (cardinality == 2, highly imbalanced):

```python
>>> bug_df.loc[0, "feature"] = 0
>>> _estimate_mi(bug_df[["feature"]].values, bug_df["target"], discrete_target=True, discrete_features=True)
array([9.21535662e-06])
>>> _estimate_mi(bug_df[["feature"]].values, bug_df["target"], discrete_target=True, discrete_features=False)
array([0.16500045])
>>> _estimate_mi(bug_df[["feature"]].values, bug_df["target"], discrete_target=True, discrete_features=False, n_neighbors=10)
array([0.03688242])
>>> _estimate_mi(bug_df[["feature"]].values, bug_df["target"], discrete_target=True, discrete_features=False, n_neighbors=30)
array([0.01116084])
```

So treating is as a discrete feature resolves the problem as expected. Treating it as a continuous feature with a larger value of `n_neighbors` decreases the magnitude of the problem but makes the code run even slower (and probably also use more memory). So tweaking `n_neighbors` alone is most likely not the solution.

Note that `_estimate_mi` adds some noise to continuous features internally. Calling `_compute_mi_cd` directly (without the noise) does not have this problem, but the code becomes extremely slow.

Labels: Bug, Needs Investigation