Repository: scverse/scanpy

Issue:
 Linkage 'Z' contains negative distances.
### Please make sure these conditions are met

- [X] I have checked that this issue has not already been reported.
- [X] I have confirmed this bug exists on the latest version of scanpy.
- [ ] (optional) I have confirmed this bug exists on the master branch of scanpy.

### What happened?

I'm encountering an error when running the sc.pl.rank_genes_groups_heatmap function in the scanpy package. The error message is "Linkage 'Z' contains negative distances." What could be causing this error and how can I fix it?

### Minimal code sample

```python
sc.pl.rank_genes_groups_heatmap(adata,  n_genes=10, groupby='clusters',show_gene_labels=True,save='cluster.markers.heatmap.svg')
```


### Error output

```pytb
sc.pl.rank_genes_groups_heatmap(adata,  n_genes=10, groupby=cluster,show_gene_labels=True,save=(id+'_processed.top10.cluster.markers.heatmap.svg'))
  File "/opt/conda/envs/st/lib/python3.8/site-packages/scanpy/plotting/_tools/__init__.py", line 673, in rank_genes_groups_heatmap
    return _rank_genes_groups_plot(
  File "/opt/conda/envs/st/lib/python3.8/site-packages/scanpy/plotting/_tools/__init__.py", line 592, in _rank_genes_groups_plot
    return heatmap(
  File "/opt/conda/envs/st/lib/python3.8/site-packages/scanpy/plotting/_anndata.py", line 1087, in heatmap
    dendro_data = _reorder_categories_after_dendrogram(
  File "/opt/conda/envs/st/lib/python3.8/site-packages/scanpy/plotting/_anndata.py", line 2134, in _reorder_categories_after_dendrogram
    key = _get_dendrogram_key(adata, dendrogram, groupby)
  File "/opt/conda/envs/st/lib/python3.8/site-packages/scanpy/plotting/_anndata.py", line 2236, in _get_dendrogram_key
    dendrogram(adata, groupby, key_added=dendrogram_key)
  File "/opt/conda/envs/st/lib/python3.8/site-packages/scanpy/tools/_dendrogram.py", line 143, in dendrogram
    dendro_info = sch.dendrogram(z_var, labels=list(categories), no_plot=True)
  File "/opt/conda/envs/st/lib/python3.8/site-packages/scipy/cluster/hierarchy.py", line 3301, in dendrogram
    is_valid_linkage(Z, throw=True, name='Z')
  File "/opt/conda/envs/st/lib/python3.8/site-packages/scipy/cluster/hierarchy.py", line 2280, in is_valid_linkage
    raise ValueError('Linkage %scontains negative distances.' %
ValueError: Linkage 'Z' contains negative distances.
```


### Versions

<details>

```
-----
anndata     0.8.0
scanpy      1.9.3
-----
PIL                 9.4.0
asciitree           NA
beta_ufunc          NA
binom_ufunc         NA
cairocffi           1.6.1
cffi                1.15.1
cloudpickle         2.2.1
colorama            0.4.6
cycler              0.10.0
cython_runtime      NA
cytoolz             0.12.0
dask                2022.11.1
dateutil            2.8.2
defusedxml          0.7.1
entrypoints         0.4
fasteners           0.17.3
fsspec              2023.6.0
google              NA
h5py                3.7.0
igraph              0.9.11
jinja2              3.0.3
joblib              1.2.0
kiwisolver          1.4.4
leidenalg           0.8.10
llvmlite            0.39.1
louvain             0.7.1
lz4                 4.3.2
markupsafe          2.1.3
matplotlib          3.5.2
mpl_toolkits        NA
msgpack             1.0.5
natsort             8.2.0
nbinom_ufunc        NA
numba               0.56.4
numcodecs           0.11.0
numexpr             2.8.4
numpy               1.21.6
packaging           23.1
pandas              1.5.3
pkg_resources       NA
psutil              5.9.5
pyarrow             8.0.0
pycparser           2.21
pyparsing           3.1.0
pytz                2023.3
scipy               1.7.3
session_info        1.0.0
setuptools          68.0.0
setuptools_scm      NA
six                 1.16.0
sklearn             1.0.1
snappy              NA
sphinxcontrib       NA
tblib               1.7.0
texttable           1.6.7
threadpoolctl       3.2.0
tlz                 0.12.0
toolz               0.12.0
typing_extensions   NA
wcwidth             0.2.6
yaml                6.0
zarr                2.15.0
zipp                NA
-----
Python 3.8.15 | packaged by conda-forge | (default, Nov 22 2022, 08:46:39) [GCC 10.4.0]
Linux-3.10.0-1127.el7.x86_64-x86_64-with-glibc2.10
-----

```

</details>



Discussion:
This problem is caused by the inclusion of the floating point number 1 in the corr_matrix. Due to the limited precision of floating point numbers, 1 may be greater than 1 when converted to a binary floating point number and then converted to decimal.
In `scanpy/tools/_dendrogram.py`，add

```py
corr_matrix = mean_df.T.corr(method=cor_method)
corr_matrix = np.where(corr_matrix > 1, 1, corr_matrix )
```

to solve this problem


Resolving patch:
diff --git a/scanpy/tools/_dendrogram.py b/scanpy/tools/_dendrogram.py
--- a/scanpy/tools/_dendrogram.py
+++ b/scanpy/tools/_dendrogram.py
@@ -145,7 +145,7 @@ def dendrogram(
         categorical.name = "_".join(groupby)
 
         rep_df.set_index(categorical, inplace=True)
-        categories = rep_df.index.categories
+        categories: pd.Index = rep_df.index.categories
     else:
         gene_names = adata.raw.var_names if use_raw else adata.var_names
         from ..plotting._anndata import _prepare_dataframe
@@ -164,7 +164,7 @@ def dendrogram(
     import scipy.cluster.hierarchy as sch
     from scipy.spatial import distance
 
-    corr_matrix = mean_df.T.corr(method=cor_method)
+    corr_matrix = mean_df.T.corr(method=cor_method).clip(-1, 1)
     corr_condensed = distance.squareform(1 - corr_matrix)
     z_var = sch.linkage(
         corr_condensed, method=linkage_method, optimal_ordering=optimal_ordering


Test patch:
diff --git a/scanpy/tests/test_dendrogram.py b/scanpy/tests/test_dendrogram.py
new file mode 100644
--- /dev/null
+++ b/scanpy/tests/test_dendrogram.py
@@ -0,0 +1,76 @@
+from __future__ import annotations
+
+import numpy as np
+import pandas as pd
+import pytest
+import scipy.sparse as sp
+
+import scanpy as sc
+from scanpy.testing._helpers.data import pbmc68k_reduced
+
+n_neighbors = 5
+key = "test"
+
+
+@pytest.mark.parametrize("groupby", ["bulk_labels", ["bulk_labels", "phase"]])
+@pytest.mark.parametrize("key_added", [None, "custom_key"])
+def test_dendrogram_key_added(groupby, key_added):
+    adata = pbmc68k_reduced()
+    sc.tl.dendrogram(adata, groupby=groupby, key_added=key_added, use_rep="X_pca")
+    if isinstance(groupby, list):
+        dendrogram_key = f'dendrogram_{"_".join(groupby)}'
+    else:
+        dendrogram_key = f"dendrogram_{groupby}"
+
+    if key_added is None:
+        key_added = dendrogram_key
+    assert key_added in adata.uns
+
+
+REP_PCA_0 = [
+    *(1.50808525e00, -1.67258829e-01, -7.12063432e-01, -2.07935140e-01),
+    *(-3.55730444e-01, -2.24421427e-01, -1.46907698e-02, -7.01090470e-02),
+    *(-1.31467551e-01, -3.75757217e-02, -1.07698059e-02, -4.37555499e-02),
+    *(1.06897885e-02, 1.10454357e-03, -5.37674241e-02, -4.94170748e-03),
+    *(1.11988001e-02, -4.48330259e-03, -2.56892946e-02, -3.50749046e-02),
+    *(-3.15931924e-02, 2.84416862e-02, -3.70664597e-02, -2.38820408e-02),
+    *(-4.57040370e-02, 2.99325008e-02, 9.56365839e-03, -4.28026691e-02),
+    *(5.36734704e-03, -3.08445804e-02, -1.16719725e-02, -2.35078149e-02),
+    *(2.87542702e-04, -1.70532353e-02, -1.79676879e-02, -3.09410989e-02),
+    *(-1.09178647e-02, -1.60753895e-02, -1.04769412e-02, -1.36501975e-02),
+    *(-6.83976896e-03, -1.17562497e-02, -4.65345643e-02, 1.91588048e-02),
+    *(-1.38043752e-02, 4.75460896e-03, -1.41307563e-02, -1.03387292e-02),
+    *(-1.68043356e-02, 1.33516011e-03),
+]
+REP_PCA_1_6 = [
+    *(-2.70745814e-01, -3.45929652e-01, 6.27844110e-02, -8.34012777e-02),
+    *(-1.08290315e-01, -1.38125733e-01, -2.57148240e-02, -2.73127705e-02),
+    *(-1.45030200e-01, -6.88858554e-02, -4.28490154e-02, -1.88931823e-02),
+    *(-2.56232135e-02, -7.66322482e-03, -5.49384989e-02, -1.43514248e-02),
+    *(2.42769364e-02, -3.01547404e-02, -3.37253511e-02, -3.81337740e-02),
+    *(-3.42049589e-03, -4.34436463e-03, -4.15385924e-02, -2.66448390e-02),
+    *(-2.74285320e-02, 1.47806173e-02, 1.19129466e-02, -6.70884028e-02),
+    *(2.58150720e-03, -1.64280720e-02, -1.07431635e-02, -3.04328315e-02),
+    *(-3.82748269e-03, -2.95090005e-02, -3.10521629e-02, -3.43420058e-02),
+    *(-4.49432433e-03, -2.15906072e-02, -1.23507539e-02, -2.88041346e-02),
+    *(-7.31994957e-03, -7.28111062e-03, -7.61008039e-02, 2.40524579e-02),
+    *(-1.20806806e-02, 5.05997473e-03, -2.53410172e-02, -1.83318909e-02),
+    *(-1.81263424e-02, -3.35110351e-03),
+]
+REP_PCA = np.array([REP_PCA_0, *([REP_PCA_1_6] * 6)], dtype=np.float32)
+
+
+def test_dendrogram_cor():
+    rep = sc.AnnData(
+        sp.csr_matrix(
+            (
+                np.array([1.2762934659055623, 1.6916760106710726, 1.6916760106710726]),
+                np.array([12, 5, 44]),
+                np.array([0, 0, 0, 0, 0, 1, 2, 3]),
+            ),
+            shape=(7, 51),
+        ),
+        dict(leiden=pd.Categorical(["372", "366", "357", "357", "357", "357", "357"])),
+        obsm=dict(X_pca=REP_PCA),
+    )
+    sc.tl.dendrogram(rep, groupby="leiden")
diff --git a/scanpy/tests/test_dendrogram_key_added.py b/scanpy/tests/test_dendrogram_key_added.py
deleted file mode 100644
--- a/scanpy/tests/test_dendrogram_key_added.py
+++ /dev/null
@@ -1,24 +0,0 @@
-from __future__ import annotations
-
-import pytest
-
-import scanpy as sc
-from scanpy.testing._helpers.data import pbmc68k_reduced
-
-n_neighbors = 5
-key = "test"
-
-
-@pytest.mark.parametrize("groupby", ["bulk_labels", ["bulk_labels", "phase"]])
-@pytest.mark.parametrize("key_added", [None, "custom_key"])
-def test_dendrogram_key_added(groupby, key_added):
-    adata = pbmc68k_reduced()
-    sc.tl.dendrogram(adata, groupby=groupby, key_added=key_added, use_rep="X_pca")
-    if isinstance(groupby, list):
-        dendrogram_key = f'dendrogram_{"_".join(groupby)}'
-    else:
-        dendrogram_key = f"dendrogram_{groupby}"
-
-    if key_added is None:
-        key_added = dendrogram_key
-    assert key_added in adata.uns