We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN 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>


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `scanpy/tools/_dendrogram.py`
Content:
```
1 """
2 Computes a dendrogram based on a given categorical observation.
3 """
4 
5 from __future__ import annotations
6 
7 from typing import TYPE_CHECKING, Any
8 
9 import pandas as pd
10 from pandas.api.types import CategoricalDtype
11 
12 from .. import logging as logg
13 from .._compat import old_positionals
14 from .._utils import _doc_params
15 from ..neighbors._doc import doc_n_pcs, doc_use_rep
16 from ._utils import _choose_representation
17 
18 if TYPE_CHECKING:
19     from collections.abc import Sequence
20 
21     from anndata import AnnData
22 
23 
24 @old_positionals(
25     "n_pcs",
26     "use_rep",
27     "var_names",
28     "use_raw",
29     "cor_method",
30     "linkage_method",
31     "optimal_ordering",
32     "key_added",
33     "inplace",
34 )
35 @_doc_params(n_pcs=doc_n_pcs, use_rep=doc_use_rep)
36 def dendrogram(
37     adata: AnnData,
38     groupby: str | Sequence[str],
39     *,
40     n_pcs: int | None = None,
41     use_rep: str | None = None,
42     var_names: Sequence[str] | None = None,
43     use_raw: bool | None = None,
44     cor_method: str = "pearson",
45     linkage_method: str = "complete",
46     optimal_ordering: bool = False,
47     key_added: str | None = None,
48     inplace: bool = True,
49 ) -> dict[str, Any] | None:
50     """\
51     Computes a hierarchical clustering for the given `groupby` categories.
52 
53     By default, the PCA representation is used unless `.X`
54     has less than 50 variables.
55 
56     Alternatively, a list of `var_names` (e.g. genes) can be given.
57 
58     Average values of either `var_names` or components are used
59     to compute a correlation matrix.
60 
61     The hierarchical clustering can be visualized using
62     :func:`scanpy.pl.dendrogram` or multiple other visualizations that can
63     include a dendrogram: :func:`~scanpy.pl.matrixplot`,
64     :func:`~scanpy.pl.heatmap`, :func:`~scanpy.pl.dotplot`,
65     and :func:`~scanpy.pl.stacked_violin`.
66 
67     .. note::
68         The computation of the hierarchical clustering is based on predefined
69         groups and not per cell. The correlation matrix is computed using by
70         default pearson but other methods are available.
71 
72     Parameters
73     ----------
74     adata
75         Annotated data matrix
76     {n_pcs}
77     {use_rep}
78     var_names
79         List of var_names to use for computing the hierarchical clustering.
80         If `var_names` is given, then `use_rep` and `n_pcs` is ignored.
81     use_raw
82         Only when `var_names` is not None.
83         Use `raw` attribute of `adata` if present.
84     cor_method
85         correlation method to use.
86         Options are 'pearson', 'kendall', and 'spearman'
87     linkage_method
88         linkage method to use. See :func:`scipy.cluster.hierarchy.linkage`
89         for more information.
90     optimal_ordering
91         Same as the optimal_ordering argument of :func:`scipy.cluster.hierarchy.linkage`
92         which reorders the linkage matrix so that the distance between successive
93         leaves is minimal.
94     key_added
95         By default, the dendrogram information is added to
96         `.uns[f'dendrogram_{{groupby}}']`.
97         Notice that the `groupby` information is added to the dendrogram.
98     inplace
99         If `True`, adds dendrogram information to `adata.uns[key_added]`,
100         else this function returns the information.
101 
102     Returns
103     -------
104     Returns `None` if `inplace=True`, else returns a `dict` with dendrogram information. Sets the following field if `inplace=True`:
105 
106     `adata.uns[f'dendrogram_{{group_by}}' | key_added]` : :class:`dict`
107         Dendrogram information.
108 
109     Examples
110     --------
111     >>> import scanpy as sc
112     >>> adata = sc.datasets.pbmc68k_reduced()
113     >>> sc.tl.dendrogram(adata, groupby='bulk_labels')
114     >>> sc.pl.dendrogram(adata, groupby='bulk_labels')  # doctest: +SKIP
115     <Axes: >
116     >>> markers = ['C1QA', 'PSAP', 'CD79A', 'CD79B', 'CST3', 'LYZ']
117     >>> sc.pl.dotplot(adata, markers, groupby='bulk_labels', dendrogram=True)
118     """
119     if isinstance(groupby, str):
120         # if not a list, turn into a list
121         groupby = [groupby]
122     for group in groupby:
123         if group not in adata.obs_keys():
124             raise ValueError(
125                 "groupby has to be a valid observation. "
126                 f"Given value: {group}, valid observations: {adata.obs_keys()}"
127             )
128         if not isinstance(adata.obs[group].dtype, CategoricalDtype):
129             raise ValueError(
130                 "groupby has to be a categorical observation. "
131                 f"Given value: {group}, Column type: {adata.obs[group].dtype}"
132             )
133 
134     if var_names is None:
135         rep_df = pd.DataFrame(
136             _choose_representation(adata, use_rep=use_rep, n_pcs=n_pcs)
137         )
138         categorical = adata.obs[groupby[0]]
139         if len(groupby) > 1:
140             for group in groupby[1:]:
141                 # create new category by merging the given groupby categories
142                 categorical = (
143                     categorical.astype(str) + "_" + adata.obs[group].astype(str)
144                 ).astype("category")
145         categorical.name = "_".join(groupby)
146 
147         rep_df.set_index(categorical, inplace=True)
148         categories = rep_df.index.categories
149     else:
150         gene_names = adata.raw.var_names if use_raw else adata.var_names
151         from ..plotting._anndata import _prepare_dataframe
152 
153         categories, rep_df = _prepare_dataframe(
154             adata, gene_names, groupby, use_raw=use_raw
155         )
156 
157     # aggregate values within categories using 'mean'
158     mean_df = (
159         rep_df.groupby(level=0, observed=True)
160         .mean()
161         .loc[categories]  # Fixed ordering for pandas < 2
162     )
163 
164     import scipy.cluster.hierarchy as sch
165     from scipy.spatial import distance
166 
167     corr_matrix = mean_df.T.corr(method=cor_method)
168     corr_condensed = distance.squareform(1 - corr_matrix)
169     z_var = sch.linkage(
170         corr_condensed, method=linkage_method, optimal_ordering=optimal_ordering
171     )
172     dendro_info = sch.dendrogram(z_var, labels=list(categories), no_plot=True)
173 
174     dat = dict(
175         linkage=z_var,
176         groupby=groupby,
177         use_rep=use_rep,
178         cor_method=cor_method,
179         linkage_method=linkage_method,
180         categories_ordered=dendro_info["ivl"],
181         categories_idx_ordered=dendro_info["leaves"],
182         dendrogram_info=dendro_info,
183         correlation_matrix=corr_matrix.values,
184     )
185 
186     if inplace:
187         if key_added is None:
188             key_added = f'dendrogram_{"_".join(groupby)}'
189         logg.info(f"Storing dendrogram info using `.uns[{key_added!r}]`")
190         adata.uns[key_added] = dat
191     else:
192         return dat
193
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

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 @@
         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 @@
     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

{"golden_diff": "diff --git a/scanpy/tools/_dendrogram.py b/scanpy/tools/_dendrogram.py\n--- a/scanpy/tools/_dendrogram.py\n+++ b/scanpy/tools/_dendrogram.py\n@@ -145,7 +145,7 @@\n         categorical.name = \"_\".join(groupby)\n \n         rep_df.set_index(categorical, inplace=True)\n-        categories = rep_df.index.categories\n+        categories: pd.Index = rep_df.index.categories\n     else:\n         gene_names = adata.raw.var_names if use_raw else adata.var_names\n         from ..plotting._anndata import _prepare_dataframe\n@@ -164,7 +164,7 @@\n     import scipy.cluster.hierarchy as sch\n     from scipy.spatial import distance\n \n-    corr_matrix = mean_df.T.corr(method=cor_method)\n+    corr_matrix = mean_df.T.corr(method=cor_method).clip(-1, 1)\n     corr_condensed = distance.squareform(1 - corr_matrix)\n     z_var = sch.linkage(\n         corr_condensed, method=linkage_method, optimal_ordering=optimal_ordering\n", "issue": " Linkage 'Z' contains negative distances.\n### Please make sure these conditions are met\n\n- [X] I have checked that this issue has not already been reported.\n- [X] I have confirmed this bug exists on the latest version of scanpy.\n- [ ] (optional) I have confirmed this bug exists on the master branch of scanpy.\n\n### What happened?\n\nI'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?\n\n### Minimal code sample\n\n```python\nsc.pl.rank_genes_groups_heatmap(adata,  n_genes=10, groupby='clusters',show_gene_labels=True,save='cluster.markers.heatmap.svg')\n```\n\n\n### Error output\n\n```pytb\nsc.pl.rank_genes_groups_heatmap(adata,  n_genes=10, groupby=cluster,show_gene_labels=True,save=(id+'_processed.top10.cluster.markers.heatmap.svg'))\r\n  File \"/opt/conda/envs/st/lib/python3.8/site-packages/scanpy/plotting/_tools/__init__.py\", line 673, in rank_genes_groups_heatmap\r\n    return _rank_genes_groups_plot(\r\n  File \"/opt/conda/envs/st/lib/python3.8/site-packages/scanpy/plotting/_tools/__init__.py\", line 592, in _rank_genes_groups_plot\r\n    return heatmap(\r\n  File \"/opt/conda/envs/st/lib/python3.8/site-packages/scanpy/plotting/_anndata.py\", line 1087, in heatmap\r\n    dendro_data = _reorder_categories_after_dendrogram(\r\n  File \"/opt/conda/envs/st/lib/python3.8/site-packages/scanpy/plotting/_anndata.py\", line 2134, in _reorder_categories_after_dendrogram\r\n    key = _get_dendrogram_key(adata, dendrogram, groupby)\r\n  File \"/opt/conda/envs/st/lib/python3.8/site-packages/scanpy/plotting/_anndata.py\", line 2236, in _get_dendrogram_key\r\n    dendrogram(adata, groupby, key_added=dendrogram_key)\r\n  File \"/opt/conda/envs/st/lib/python3.8/site-packages/scanpy/tools/_dendrogram.py\", line 143, in dendrogram\r\n    dendro_info = sch.dendrogram(z_var, labels=list(categories), no_plot=True)\r\n  File \"/opt/conda/envs/st/lib/python3.8/site-packages/scipy/cluster/hierarchy.py\", line 3301, in dendrogram\r\n    is_valid_linkage(Z, throw=True, name='Z')\r\n  File \"/opt/conda/envs/st/lib/python3.8/site-packages/scipy/cluster/hierarchy.py\", line 2280, in is_valid_linkage\r\n    raise ValueError('Linkage %scontains negative distances.' %\r\nValueError: Linkage 'Z' contains negative distances.\n```\n\n\n### Versions\n\n<details>\r\n\r\n```\r\n-----\r\nanndata     0.8.0\r\nscanpy      1.9.3\r\n-----\r\nPIL                 9.4.0\r\nasciitree           NA\r\nbeta_ufunc          NA\r\nbinom_ufunc         NA\r\ncairocffi           1.6.1\r\ncffi                1.15.1\r\ncloudpickle         2.2.1\r\ncolorama            0.4.6\r\ncycler              0.10.0\r\ncython_runtime      NA\r\ncytoolz             0.12.0\r\ndask                2022.11.1\r\ndateutil            2.8.2\r\ndefusedxml          0.7.1\r\nentrypoints         0.4\r\nfasteners           0.17.3\r\nfsspec              2023.6.0\r\ngoogle              NA\r\nh5py                3.7.0\r\nigraph              0.9.11\r\njinja2              3.0.3\r\njoblib              1.2.0\r\nkiwisolver          1.4.4\r\nleidenalg           0.8.10\r\nllvmlite            0.39.1\r\nlouvain             0.7.1\r\nlz4                 4.3.2\r\nmarkupsafe          2.1.3\r\nmatplotlib          3.5.2\r\nmpl_toolkits        NA\r\nmsgpack             1.0.5\r\nnatsort             8.2.0\r\nnbinom_ufunc        NA\r\nnumba               0.56.4\r\nnumcodecs           0.11.0\r\nnumexpr             2.8.4\r\nnumpy               1.21.6\r\npackaging           23.1\r\npandas              1.5.3\r\npkg_resources       NA\r\npsutil              5.9.5\r\npyarrow             8.0.0\r\npycparser           2.21\r\npyparsing           3.1.0\r\npytz                2023.3\r\nscipy               1.7.3\r\nsession_info        1.0.0\r\nsetuptools          68.0.0\r\nsetuptools_scm      NA\r\nsix                 1.16.0\r\nsklearn             1.0.1\r\nsnappy              NA\r\nsphinxcontrib       NA\r\ntblib               1.7.0\r\ntexttable           1.6.7\r\nthreadpoolctl       3.2.0\r\ntlz                 0.12.0\r\ntoolz               0.12.0\r\ntyping_extensions   NA\r\nwcwidth             0.2.6\r\nyaml                6.0\r\nzarr                2.15.0\r\nzipp                NA\r\n-----\r\nPython 3.8.15 | packaged by conda-forge | (default, Nov 22 2022, 08:46:39) [GCC 10.4.0]\r\nLinux-3.10.0-1127.el7.x86_64-x86_64-with-glibc2.10\r\n-----\r\n\r\n```\r\n\r\n</details>\r\n\n", "before_files": [{"content": "\"\"\"\nComputes a dendrogram based on a given categorical observation.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import TYPE_CHECKING, Any\n\nimport pandas as pd\nfrom pandas.api.types import CategoricalDtype\n\nfrom .. import logging as logg\nfrom .._compat import old_positionals\nfrom .._utils import _doc_params\nfrom ..neighbors._doc import doc_n_pcs, doc_use_rep\nfrom ._utils import _choose_representation\n\nif TYPE_CHECKING:\n    from collections.abc import Sequence\n\n    from anndata import AnnData\n\n\n@old_positionals(\n    \"n_pcs\",\n    \"use_rep\",\n    \"var_names\",\n    \"use_raw\",\n    \"cor_method\",\n    \"linkage_method\",\n    \"optimal_ordering\",\n    \"key_added\",\n    \"inplace\",\n)\n@_doc_params(n_pcs=doc_n_pcs, use_rep=doc_use_rep)\ndef dendrogram(\n    adata: AnnData,\n    groupby: str | Sequence[str],\n    *,\n    n_pcs: int | None = None,\n    use_rep: str | None = None,\n    var_names: Sequence[str] | None = None,\n    use_raw: bool | None = None,\n    cor_method: str = \"pearson\",\n    linkage_method: str = \"complete\",\n    optimal_ordering: bool = False,\n    key_added: str | None = None,\n    inplace: bool = True,\n) -> dict[str, Any] | None:\n    \"\"\"\\\n    Computes a hierarchical clustering for the given `groupby` categories.\n\n    By default, the PCA representation is used unless `.X`\n    has less than 50 variables.\n\n    Alternatively, a list of `var_names` (e.g. genes) can be given.\n\n    Average values of either `var_names` or components are used\n    to compute a correlation matrix.\n\n    The hierarchical clustering can be visualized using\n    :func:`scanpy.pl.dendrogram` or multiple other visualizations that can\n    include a dendrogram: :func:`~scanpy.pl.matrixplot`,\n    :func:`~scanpy.pl.heatmap`, :func:`~scanpy.pl.dotplot`,\n    and :func:`~scanpy.pl.stacked_violin`.\n\n    .. note::\n        The computation of the hierarchical clustering is based on predefined\n        groups and not per cell. The correlation matrix is computed using by\n        default pearson but other methods are available.\n\n    Parameters\n    ----------\n    adata\n        Annotated data matrix\n    {n_pcs}\n    {use_rep}\n    var_names\n        List of var_names to use for computing the hierarchical clustering.\n        If `var_names` is given, then `use_rep` and `n_pcs` is ignored.\n    use_raw\n        Only when `var_names` is not None.\n        Use `raw` attribute of `adata` if present.\n    cor_method\n        correlation method to use.\n        Options are 'pearson', 'kendall', and 'spearman'\n    linkage_method\n        linkage method to use. See :func:`scipy.cluster.hierarchy.linkage`\n        for more information.\n    optimal_ordering\n        Same as the optimal_ordering argument of :func:`scipy.cluster.hierarchy.linkage`\n        which reorders the linkage matrix so that the distance between successive\n        leaves is minimal.\n    key_added\n        By default, the dendrogram information is added to\n        `.uns[f'dendrogram_{{groupby}}']`.\n        Notice that the `groupby` information is added to the dendrogram.\n    inplace\n        If `True`, adds dendrogram information to `adata.uns[key_added]`,\n        else this function returns the information.\n\n    Returns\n    -------\n    Returns `None` if `inplace=True`, else returns a `dict` with dendrogram information. Sets the following field if `inplace=True`:\n\n    `adata.uns[f'dendrogram_{{group_by}}' | key_added]` : :class:`dict`\n        Dendrogram information.\n\n    Examples\n    --------\n    >>> import scanpy as sc\n    >>> adata = sc.datasets.pbmc68k_reduced()\n    >>> sc.tl.dendrogram(adata, groupby='bulk_labels')\n    >>> sc.pl.dendrogram(adata, groupby='bulk_labels')  # doctest: +SKIP\n    <Axes: >\n    >>> markers = ['C1QA', 'PSAP', 'CD79A', 'CD79B', 'CST3', 'LYZ']\n    >>> sc.pl.dotplot(adata, markers, groupby='bulk_labels', dendrogram=True)\n    \"\"\"\n    if isinstance(groupby, str):\n        # if not a list, turn into a list\n        groupby = [groupby]\n    for group in groupby:\n        if group not in adata.obs_keys():\n            raise ValueError(\n                \"groupby has to be a valid observation. \"\n                f\"Given value: {group}, valid observations: {adata.obs_keys()}\"\n            )\n        if not isinstance(adata.obs[group].dtype, CategoricalDtype):\n            raise ValueError(\n                \"groupby has to be a categorical observation. \"\n                f\"Given value: {group}, Column type: {adata.obs[group].dtype}\"\n            )\n\n    if var_names is None:\n        rep_df = pd.DataFrame(\n            _choose_representation(adata, use_rep=use_rep, n_pcs=n_pcs)\n        )\n        categorical = adata.obs[groupby[0]]\n        if len(groupby) > 1:\n            for group in groupby[1:]:\n                # create new category by merging the given groupby categories\n                categorical = (\n                    categorical.astype(str) + \"_\" + adata.obs[group].astype(str)\n                ).astype(\"category\")\n        categorical.name = \"_\".join(groupby)\n\n        rep_df.set_index(categorical, inplace=True)\n        categories = rep_df.index.categories\n    else:\n        gene_names = adata.raw.var_names if use_raw else adata.var_names\n        from ..plotting._anndata import _prepare_dataframe\n\n        categories, rep_df = _prepare_dataframe(\n            adata, gene_names, groupby, use_raw=use_raw\n        )\n\n    # aggregate values within categories using 'mean'\n    mean_df = (\n        rep_df.groupby(level=0, observed=True)\n        .mean()\n        .loc[categories]  # Fixed ordering for pandas < 2\n    )\n\n    import scipy.cluster.hierarchy as sch\n    from scipy.spatial import distance\n\n    corr_matrix = mean_df.T.corr(method=cor_method)\n    corr_condensed = distance.squareform(1 - corr_matrix)\n    z_var = sch.linkage(\n        corr_condensed, method=linkage_method, optimal_ordering=optimal_ordering\n    )\n    dendro_info = sch.dendrogram(z_var, labels=list(categories), no_plot=True)\n\n    dat = dict(\n        linkage=z_var,\n        groupby=groupby,\n        use_rep=use_rep,\n        cor_method=cor_method,\n        linkage_method=linkage_method,\n        categories_ordered=dendro_info[\"ivl\"],\n        categories_idx_ordered=dendro_info[\"leaves\"],\n        dendrogram_info=dendro_info,\n        correlation_matrix=corr_matrix.values,\n    )\n\n    if inplace:\n        if key_added is None:\n            key_added = f'dendrogram_{\"_\".join(groupby)}'\n        logg.info(f\"Storing dendrogram info using `.uns[{key_added!r}]`\")\n        adata.uns[key_added] = dat\n    else:\n        return dat\n", "path": "scanpy/tools/_dendrogram.py"}], "after_files": [{"content": "\"\"\"\nComputes a dendrogram based on a given categorical observation.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import TYPE_CHECKING, Any\n\nimport pandas as pd\nfrom pandas.api.types import CategoricalDtype\n\nfrom .. import logging as logg\nfrom .._compat import old_positionals\nfrom .._utils import _doc_params\nfrom ..neighbors._doc import doc_n_pcs, doc_use_rep\nfrom ._utils import _choose_representation\n\nif TYPE_CHECKING:\n    from collections.abc import Sequence\n\n    from anndata import AnnData\n\n\n@old_positionals(\n    \"n_pcs\",\n    \"use_rep\",\n    \"var_names\",\n    \"use_raw\",\n    \"cor_method\",\n    \"linkage_method\",\n    \"optimal_ordering\",\n    \"key_added\",\n    \"inplace\",\n)\n@_doc_params(n_pcs=doc_n_pcs, use_rep=doc_use_rep)\ndef dendrogram(\n    adata: AnnData,\n    groupby: str | Sequence[str],\n    *,\n    n_pcs: int | None = None,\n    use_rep: str | None = None,\n    var_names: Sequence[str] | None = None,\n    use_raw: bool | None = None,\n    cor_method: str = \"pearson\",\n    linkage_method: str = \"complete\",\n    optimal_ordering: bool = False,\n    key_added: str | None = None,\n    inplace: bool = True,\n) -> dict[str, Any] | None:\n    \"\"\"\\\n    Computes a hierarchical clustering for the given `groupby` categories.\n\n    By default, the PCA representation is used unless `.X`\n    has less than 50 variables.\n\n    Alternatively, a list of `var_names` (e.g. genes) can be given.\n\n    Average values of either `var_names` or components are used\n    to compute a correlation matrix.\n\n    The hierarchical clustering can be visualized using\n    :func:`scanpy.pl.dendrogram` or multiple other visualizations that can\n    include a dendrogram: :func:`~scanpy.pl.matrixplot`,\n    :func:`~scanpy.pl.heatmap`, :func:`~scanpy.pl.dotplot`,\n    and :func:`~scanpy.pl.stacked_violin`.\n\n    .. note::\n        The computation of the hierarchical clustering is based on predefined\n        groups and not per cell. The correlation matrix is computed using by\n        default pearson but other methods are available.\n\n    Parameters\n    ----------\n    adata\n        Annotated data matrix\n    {n_pcs}\n    {use_rep}\n    var_names\n        List of var_names to use for computing the hierarchical clustering.\n        If `var_names` is given, then `use_rep` and `n_pcs` is ignored.\n    use_raw\n        Only when `var_names` is not None.\n        Use `raw` attribute of `adata` if present.\n    cor_method\n        correlation method to use.\n        Options are 'pearson', 'kendall', and 'spearman'\n    linkage_method\n        linkage method to use. See :func:`scipy.cluster.hierarchy.linkage`\n        for more information.\n    optimal_ordering\n        Same as the optimal_ordering argument of :func:`scipy.cluster.hierarchy.linkage`\n        which reorders the linkage matrix so that the distance between successive\n        leaves is minimal.\n    key_added\n        By default, the dendrogram information is added to\n        `.uns[f'dendrogram_{{groupby}}']`.\n        Notice that the `groupby` information is added to the dendrogram.\n    inplace\n        If `True`, adds dendrogram information to `adata.uns[key_added]`,\n        else this function returns the information.\n\n    Returns\n    -------\n    Returns `None` if `inplace=True`, else returns a `dict` with dendrogram information. Sets the following field if `inplace=True`:\n\n    `adata.uns[f'dendrogram_{{group_by}}' | key_added]` : :class:`dict`\n        Dendrogram information.\n\n    Examples\n    --------\n    >>> import scanpy as sc\n    >>> adata = sc.datasets.pbmc68k_reduced()\n    >>> sc.tl.dendrogram(adata, groupby='bulk_labels')\n    >>> sc.pl.dendrogram(adata, groupby='bulk_labels')  # doctest: +SKIP\n    <Axes: >\n    >>> markers = ['C1QA', 'PSAP', 'CD79A', 'CD79B', 'CST3', 'LYZ']\n    >>> sc.pl.dotplot(adata, markers, groupby='bulk_labels', dendrogram=True)\n    \"\"\"\n    if isinstance(groupby, str):\n        # if not a list, turn into a list\n        groupby = [groupby]\n    for group in groupby:\n        if group not in adata.obs_keys():\n            raise ValueError(\n                \"groupby has to be a valid observation. \"\n                f\"Given value: {group}, valid observations: {adata.obs_keys()}\"\n            )\n        if not isinstance(adata.obs[group].dtype, CategoricalDtype):\n            raise ValueError(\n                \"groupby has to be a categorical observation. \"\n                f\"Given value: {group}, Column type: {adata.obs[group].dtype}\"\n            )\n\n    if var_names is None:\n        rep_df = pd.DataFrame(\n            _choose_representation(adata, use_rep=use_rep, n_pcs=n_pcs)\n        )\n        categorical = adata.obs[groupby[0]]\n        if len(groupby) > 1:\n            for group in groupby[1:]:\n                # create new category by merging the given groupby categories\n                categorical = (\n                    categorical.astype(str) + \"_\" + adata.obs[group].astype(str)\n                ).astype(\"category\")\n        categorical.name = \"_\".join(groupby)\n\n        rep_df.set_index(categorical, inplace=True)\n        categories: pd.Index = rep_df.index.categories\n    else:\n        gene_names = adata.raw.var_names if use_raw else adata.var_names\n        from ..plotting._anndata import _prepare_dataframe\n\n        categories, rep_df = _prepare_dataframe(\n            adata, gene_names, groupby, use_raw=use_raw\n        )\n\n    # aggregate values within categories using 'mean'\n    mean_df = (\n        rep_df.groupby(level=0, observed=True)\n        .mean()\n        .loc[categories]  # Fixed ordering for pandas < 2\n    )\n\n    import scipy.cluster.hierarchy as sch\n    from scipy.spatial import distance\n\n    corr_matrix = mean_df.T.corr(method=cor_method).clip(-1, 1)\n    corr_condensed = distance.squareform(1 - corr_matrix)\n    z_var = sch.linkage(\n        corr_condensed, method=linkage_method, optimal_ordering=optimal_ordering\n    )\n    dendro_info = sch.dendrogram(z_var, labels=list(categories), no_plot=True)\n\n    dat = dict(\n        linkage=z_var,\n        groupby=groupby,\n        use_rep=use_rep,\n        cor_method=cor_method,\n        linkage_method=linkage_method,\n        categories_ordered=dendro_info[\"ivl\"],\n        categories_idx_ordered=dendro_info[\"leaves\"],\n        dendrogram_info=dendro_info,\n        correlation_matrix=corr_matrix.values,\n    )\n\n    if inplace:\n        if key_added is None:\n            key_added = f'dendrogram_{\"_\".join(groupby)}'\n        logg.info(f\"Storing dendrogram info using `.uns[{key_added!r}]`\")\n        adata.uns[key_added] = dat\n    else:\n        return dat\n", "path": "scanpy/tools/_dendrogram.py"}]}