From 40dc2c3bedaadfd6b6ed4e418d9cc31f8f4c11e0 Mon Sep 17 00:00:00 2001 From: Zethson Date: Wed, 24 Feb 2021 13:23:43 +0100 Subject: [PATCH 01/85] add flake8 pre-commit Signed-off-by: Zethson --- .flake8 | 4 + .pre-commit-config.yaml | 5 + docs/extensions/function_images.py | 4 +- docs/extensions/github_links.py | 4 +- pyproject.toml | 4 +- scanpy/__init__.py | 44 ++-- scanpy/_settings.py | 25 +- scanpy/_utils.py | 82 ++----- scanpy/cli.py | 24 +- scanpy/datasets/_datasets.py | 20 +- scanpy/datasets/_ebi_expression_atlas.py | 4 +- scanpy/external/exporting.py | 74 ++---- scanpy/external/pl.py | 21 +- scanpy/external/pp/_hashsolo.py | 134 ++++------- scanpy/external/pp/_magic.py | 11 +- scanpy/external/pp/_mnn_correct.py | 11 +- scanpy/external/pp/_scanorama_integrate.py | 4 +- scanpy/external/pp/_scrublet.py | 90 +++----- scanpy/external/pp/_scvi.py | 28 +-- scanpy/external/tl/_palantir.py | 4 +- scanpy/external/tl/_phate.py | 3 +- scanpy/external/tl/_phenograph.py | 11 +- scanpy/external/tl/_trimap.py | 5 +- scanpy/external/tl/_wishbone.py | 22 +- scanpy/get/get.py | 25 +- scanpy/logging.py | 12 +- scanpy/neighbors/__init__.py | 105 +++------ scanpy/plotting/__init__.py | 2 +- scanpy/plotting/_anndata.py | 158 ++++--------- scanpy/plotting/_baseplot_class.py | 46 +--- scanpy/plotting/_dotplot.py | 35 +-- scanpy/plotting/_matrixplot.py | 6 +- scanpy/plotting/_preprocessing.py | 4 +- scanpy/plotting/_qc.py | 10 +- scanpy/plotting/_stacked_violin.py | 38 +-- scanpy/plotting/_tools/__init__.py | 48 +--- scanpy/plotting/_tools/paga.py | 144 +++--------- scanpy/plotting/_tools/scatterplots.py | 144 +++--------- scanpy/plotting/_utils.py | 82 ++----- scanpy/plotting/palettes.py | 8 +- scanpy/preprocessing/_combat.py | 33 +-- scanpy/preprocessing/_deprecated/__init__.py | 10 +- .../_deprecated/highly_variable_genes.py | 20 +- .../preprocessing/_highly_variable_genes.py | 68 ++---- scanpy/preprocessing/_normalization.py | 8 +- scanpy/preprocessing/_pca.py | 18 +- scanpy/preprocessing/_qc.py | 51 ++-- scanpy/preprocessing/_recipes.py | 16 +- scanpy/preprocessing/_simple.py | 78 ++----- scanpy/preprocessing/_utils.py | 4 +- scanpy/queries/_queries.py | 15 +- scanpy/readwrite.py | 73 ++---- scanpy/tests/conftest.py | 8 +- scanpy/tests/external/test_hashsolo.py | 4 +- scanpy/tests/external/test_wishbone.py | 4 +- scanpy/tests/helpers.py | 4 +- .../notebooks/test_paga_paul15_subsampled.py | 7 +- scanpy/tests/notebooks/test_pbmc3k.py | 15 +- scanpy/tests/test_combat.py | 4 +- scanpy/tests/test_datasets.py | 13 +- scanpy/tests/test_docs.py | 4 +- scanpy/tests/test_embedding_plots.py | 50 ++-- scanpy/tests/test_filter_rank_genes_groups.py | 4 +- scanpy/tests/test_get.py | 20 +- scanpy/tests/test_highly_variable_genes.py | 40 +--- scanpy/tests/test_ingest.py | 4 +- scanpy/tests/test_neighbors.py | 8 +- scanpy/tests/test_neighbors_key_added.py | 8 +- scanpy/tests/test_pca.py | 12 +- scanpy/tests/test_plotting.py | 59 ++--- scanpy/tests/test_preprocessing.py | 45 +--- .../tests/test_preprocessing_distributed.py | 4 +- scanpy/tests/test_qc_metrics.py | 37 +-- scanpy/tests/test_queries.py | 8 +- scanpy/tests/test_rank_genes_groups.py | 50 +--- scanpy/tests/test_rank_genes_groups_logreg.py | 4 +- scanpy/tests/test_read_10x.py | 8 +- scanpy/tests/test_scaling.py | 4 +- scanpy/tests/test_score_genes.py | 23 +- scanpy/tools/_dendrogram.py | 12 +- scanpy/tools/_diffmap.py | 4 +- scanpy/tools/_dpt.py | 218 ++++-------------- scanpy/tools/_draw_graph.py | 4 +- scanpy/tools/_embedding_density.py | 7 +- scanpy/tools/_ingest.py | 48 +--- scanpy/tools/_louvain.py | 11 +- scanpy/tools/_marker_gene_overlap.py | 52 ++--- scanpy/tools/_paga.py | 88 ++----- scanpy/tools/_rank_genes_groups.py | 106 ++++----- scanpy/tools/_score_genes.py | 20 +- scanpy/tools/_sim.py | 142 +++--------- scanpy/tools/_top_genes.py | 37 ++- scanpy/tools/_tsne_fix.py | 8 +- scanpy/tools/_umap.py | 16 +- scanpy/tools/_utils.py | 17 +- scanpy/tools/_utils_clustering.py | 12 +- setup.py | 4 +- 97 files changed, 867 insertions(+), 2289 deletions(-) create mode 100644 .flake8 diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000000..ac3ef0697a --- /dev/null +++ b/.flake8 @@ -0,0 +1,4 @@ +[flake8] +exclude = docs, scanpy/tests +max-line-length = 120 +ignore = F401, W503, E501, E203, E231, W504 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 93ae9e8fd7..97cc88d8ec 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,3 +3,8 @@ repos: rev: 20.8b1 hooks: - id: black +- repo: https://gitlab.com/pycqa/flake8 + rev: 3.8.4 + hooks: + - id: flake8 + exclude: scanpy/tests/ diff --git a/docs/extensions/function_images.py b/docs/extensions/function_images.py index 42aac73c26..f2a72840f1 100644 --- a/docs/extensions/function_images.py +++ b/docs/extensions/function_images.py @@ -6,9 +6,7 @@ from sphinx.ext.autodoc import Options -def insert_function_images( - app: Sphinx, what: str, name: str, obj: Any, options: Options, lines: List[str] -): +def insert_function_images(app: Sphinx, what: str, name: str, obj: Any, options: Options, lines: List[str]): path = app.config.api_dir / f'{name}.png' if what != 'function' or not path.is_file(): return diff --git a/docs/extensions/github_links.py b/docs/extensions/github_links.py index a2863627c0..f01106fc4b 100644 --- a/docs/extensions/github_links.py +++ b/docs/extensions/github_links.py @@ -32,9 +32,7 @@ def __call__( def register_links(app: Sphinx, config: Config): - gh_url = 'https://github.com/{github_user}/{github_repo}'.format_map( - config.html_context - ) + gh_url = 'https://github.com/{github_user}/{github_repo}'.format_map(config.html_context) app.add_role('pr', AutoLink('pr', f'{gh_url}/pull/{{}}', 'PR {}')) app.add_role('issue', AutoLink('issue', f'{gh_url}/issues/{{}}', 'issue {}')) app.add_role('noteversion', AutoLink('noteversion', f'{gh_url}/releases/tag/{{}}')) diff --git a/pyproject.toml b/pyproject.toml index 5e4e46f851..dc5963a982 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,8 +26,8 @@ source = ['scanpy'] omit = ['*/tests/*'] [tool.black] -line-length = 88 -target-version = ['py36'] +line-length = 120 +target-version = ['py38'] skip-string-normalization = true exclude = ''' /build/.* diff --git a/scanpy/__init__.py b/scanpy/__init__.py index 71eb75e977..4f2f5ef1af 100644 --- a/scanpy/__init__.py +++ b/scanpy/__init__.py @@ -1,23 +1,9 @@ """Single-Cell Analysis in Python.""" -from ._metadata import __version__, __author__, __email__ - -from ._utils import check_versions - -check_versions() -del check_versions - -# the actual API -from ._settings import ( - settings, - Verbosity, -) # start with settings as several tools are using it -from . import tools as tl -from . import preprocessing as pp -from . import plotting as pl -from . import datasets, logging, queries, external, get - -from anndata import AnnData, concat +from ._utils import annotate_doc_types +import sys +from .neighbors import Neighbors +from .readwrite import read, read_10x_h5, read_10x_mtx, write, read_visium from anndata import ( read_h5ad, read_csv, @@ -28,16 +14,30 @@ read_text, read_umi_tools, ) -from .readwrite import read, read_10x_h5, read_10x_mtx, write, read_visium -from .neighbors import Neighbors +from anndata import AnnData, concat +from . import datasets, logging, queries, external, get +from . import plotting as pl +from . import preprocessing as pp +from . import tools as tl +from ._settings import ( + settings, + Verbosity, +) # start with settings as several tools are using it +from ._metadata import __version__, __author__, __email__ + +from ._utils import check_versions + +check_versions() +del check_versions + +# the actual API + set_figure_params = settings.set_figure_params # has to be done at the end, after everything has been imported -import sys sys.modules.update({f'{__name__}.{m}': globals()[m] for m in ['tl', 'pp', 'pl']}) -from ._utils import annotate_doc_types annotate_doc_types(sys.modules[__name__], 'scanpy') del sys, annotate_doc_types diff --git a/scanpy/_settings.py b/scanpy/_settings.py index e217b36083..fdb4ba3c10 100644 --- a/scanpy/_settings.py +++ b/scanpy/_settings.py @@ -53,9 +53,7 @@ def _type_check(var: Any, varname: str, types: Union[type, Tuple[type, ...]]): possible_types_str = types.__name__ else: type_names = [t.__name__ for t in types] - possible_types_str = "{} or {}".format( - ", ".join(type_names[:-1]), type_names[-1] - ) + possible_types_str = "{} or {}".format(", ".join(type_names[:-1]), type_names[-1]) raise TypeError(f"{varname} must be of type {possible_types_str}") @@ -141,9 +139,7 @@ def verbosity(self) -> Verbosity: @verbosity.setter def verbosity(self, verbosity: Union[Verbosity, int, str]): - verbosity_str_options = [ - v for v in _VERBOSITY_TO_LOGLEVEL if isinstance(v, str) - ] + verbosity_str_options = [v for v in _VERBOSITY_TO_LOGLEVEL if isinstance(v, str)] if isinstance(verbosity, Verbosity): self._verbosity = verbosity elif isinstance(verbosity, int): @@ -152,8 +148,7 @@ def verbosity(self, verbosity: Union[Verbosity, int, str]): verbosity = verbosity.lower() if verbosity not in verbosity_str_options: raise ValueError( - f"Cannot set verbosity to {verbosity}. " - f"Accepted string values are: {verbosity_str_options}" + f"Cannot set verbosity to {verbosity}. " f"Accepted string values are: {verbosity_str_options}" ) else: self._verbosity = Verbosity(verbosity_str_options.index(verbosity)) @@ -185,10 +180,7 @@ def file_format_data(self, file_format: str): _type_check(file_format, "file_format_data", str) file_format_options = {"txt", "csv", "h5ad"} if file_format not in file_format_options: - raise ValueError( - f"Cannot set file_format_data to {file_format}. " - f"Must be one of {file_format_options}" - ) + raise ValueError(f"Cannot set file_format_data to {file_format}. " f"Must be one of {file_format_options}") self._file_format_data = file_format @property @@ -293,10 +285,7 @@ def cache_compression(self) -> Optional[str]: @cache_compression.setter def cache_compression(self, cache_compression: Optional[str]): if cache_compression not in {'lzf', 'gzip', None}: - raise ValueError( - f"`cache_compression` ({cache_compression}) " - "must be in {'lzf', 'gzip', None}" - ) + raise ValueError(f"`cache_compression` ({cache_compression}) " "must be in {'lzf', 'gzip', None}") self._cache_compression = cache_compression @property @@ -475,9 +464,7 @@ def _is_run_from_ipython(): def __str__(self) -> str: return '\n'.join( - f'{k} = {v!r}' - for k, v in inspect.getmembers(self) - if not k.startswith("_") and not k == 'getdoc' + f'{k} = {v!r}' for k, v in inspect.getmembers(self) if not k.startswith("_") and not k == 'getdoc' ) diff --git a/scanpy/_utils.py b/scanpy/_utils.py index 207f40f0b6..d9929dc3a0 100644 --- a/scanpy/_utils.py +++ b/scanpy/_utils.py @@ -54,9 +54,7 @@ def check_versions(): # make this a warning, not an error # it might be useful for people to still be able to run it - logg.warning( - f'Scanpy {__version__} needs umap ' f'version >=0.3.0, not {umap_version}.' - ) + logg.warning(f'Scanpy {__version__} needs umap ' f'version >=0.3.0, not {umap_version}.') def getdoc(c_or_f: Union[Callable, type]) -> Optional[str]: @@ -77,8 +75,7 @@ def type_doc(name: str): return cls return '\n'.join( - f'{line} : {type_doc(line)}' if line.strip() in sig.parameters else line - for line in doc.split('\n') + f'{line} : {type_doc(line)}' if line.strip() in sig.parameters else line for line in doc.split('\n') ) @@ -122,9 +119,7 @@ def _one_of_ours(obj, root: str): return ( hasattr(obj, "__name__") and not obj.__name__.split(".")[-1].startswith("_") - and getattr( - obj, '__module__', getattr(obj, '__qualname__', obj.__name__) - ).startswith(root) + and getattr(obj, '__module__', getattr(obj, '__qualname__', obj.__name__)).startswith(root) ) @@ -171,9 +166,7 @@ def _check_array_function_arguments(**kwargs): # TODO: Figure out a better solution for documenting dispatched functions invalid_args = [k for k, v in kwargs.items() if v is not None] if len(invalid_args) > 0: - raise TypeError( - f"Arguments {invalid_args} are only valid if an AnnData object is passed." - ) + raise TypeError(f"Arguments {invalid_args} are only valid if an AnnData object is passed.") def _check_use_raw(adata: AnnData, use_raw: Union[None, bool]) -> bool: @@ -209,12 +202,11 @@ def get_igraph_from_adjacency(adjacency, directed=None): g.add_edges(list(zip(sources, targets))) try: g.es['weight'] = weights - except: + except KeyError: pass if g.vcount() != adjacency.shape[0]: logg.warning( - f'The constructed graph has only {g.vcount()} nodes. ' - 'Your adjacency matrix contained redundant nodes.' + f'The constructed graph has only {g.vcount()} nodes. ' 'Your adjacency matrix contained redundant nodes.' ) return g @@ -281,17 +273,12 @@ def compute_association_matrix_of_groups( reference labels, entries are proportional to degree of association. """ if normalization not in {'prediction', 'reference'}: - raise ValueError( - '`normalization` needs to be either "prediction" or "reference".' - ) + raise ValueError('`normalization` needs to be either "prediction" or "reference".') sanitize_anndata(adata) cats = adata.obs[reference].cat.categories for cat in cats: if cat in settings.categories_to_ignore: - logg.info( - f'Ignoring category {cat!r} ' - 'as it’s in `settings.categories_to_ignore`.' - ) + logg.info(f'Ignoring category {cat!r} ' 'as it’s in `settings.categories_to_ignore`.') asso_names = [] asso_matrix = [] for ipred_group, pred_group in enumerate(adata.obs[prediction].cat.categories): @@ -310,15 +297,11 @@ def compute_association_matrix_of_groups( if normalization == 'prediction': # compute which fraction of the predicted group is contained in # the ref group - ratio_contained = ( - np.sum(mask_pred_int) - np.sum(mask_ref_or_pred - mask_ref) - ) / np.sum(mask_pred_int) + ratio_contained = (np.sum(mask_pred_int) - np.sum(mask_ref_or_pred - mask_ref)) / np.sum(mask_pred_int) else: # compute which fraction of the reference group is contained in # the predicted group - ratio_contained = ( - np.sum(mask_ref) - np.sum(mask_ref_or_pred - mask_pred_int) - ) / np.sum(mask_ref) + ratio_contained = (np.sum(mask_ref) - np.sum(mask_ref_or_pred - mask_pred_int)) / np.sum(mask_ref) asso_matrix[-1] += [ratio_contained] name_list_pred = [ cats[i] if cats[i] not in settings.categories_to_ignore else '' @@ -326,18 +309,13 @@ def compute_association_matrix_of_groups( if asso_matrix[-1][i] > threshold ] asso_names += ['\n'.join(name_list_pred[:max_n_names])] - Result = namedtuple( - 'compute_association_matrix_of_groups', ['asso_names', 'asso_matrix'] - ) + Result = namedtuple('compute_association_matrix_of_groups', ['asso_names', 'asso_matrix']) return Result(asso_names=asso_names, asso_matrix=np.array(asso_matrix)) def get_associated_colors_of_groups(reference_colors, asso_matrix): return [ - { - reference_colors[i_ref]: asso_matrix[i_pred, i_ref] - for i_ref in range(asso_matrix.shape[1]) - } + {reference_colors[i_ref]: asso_matrix[i_pred, i_ref] for i_ref in range(asso_matrix.shape[1])} for i_pred in range(asso_matrix.shape[0]) ] @@ -366,16 +344,9 @@ def identify_groups(ref_labels, pred_labels, return_overlaps=False): associated_predictions = {} associated_overlaps = {} for ref_label in ref_unique: - sub_pred_unique, sub_pred_counts = np.unique( - pred_labels[ref_label == ref_labels], return_counts=True - ) - relative_overlaps_pred = [ - sub_pred_counts[i] / pred_dict[n] for i, n in enumerate(sub_pred_unique) - ] - relative_overlaps_ref = [ - sub_pred_counts[i] / ref_dict[ref_label] - for i, n in enumerate(sub_pred_unique) - ] + sub_pred_unique, sub_pred_counts = np.unique(pred_labels[ref_label == ref_labels], return_counts=True) + relative_overlaps_pred = [sub_pred_counts[i] / pred_dict[n] for i, n in enumerate(sub_pred_unique)] + relative_overlaps_ref = [sub_pred_counts[i] / ref_dict[ref_label] for i, n in enumerate(sub_pred_unique)] relative_overlaps = np.c_[relative_overlaps_pred, relative_overlaps_ref] relative_overlaps_min = np.min(relative_overlaps, axis=1) pred_best_index = np.argsort(relative_overlaps_min)[::-1] @@ -499,9 +470,7 @@ def select_groups(adata, groups_order_subset='all', key='groups'): if key + '_masks' in adata.uns: groups_masks = adata.uns[key + '_masks'] else: - groups_masks = np.zeros( - (len(adata.obs[key].cat.categories), adata.obs[key].values.size), dtype=bool - ) + groups_masks = np.zeros((len(adata.obs[key].cat.categories), adata.obs[key].values.size), dtype=bool) for iname, name in enumerate(adata.obs[key].cat.categories): # if the name is not found, fallback to index retrieval if adata.obs[key].cat.categories[iname] in adata.obs[key].values: @@ -513,9 +482,7 @@ def select_groups(adata, groups_order_subset='all', key='groups'): if groups_order_subset != 'all': groups_ids = [] for name in groups_order_subset: - groups_ids.append( - np.where(adata.obs[key].cat.categories.values == name)[0][0] - ) + groups_ids.append(np.where(adata.obs[key].cat.categories.values == name)[0][0]) if len(groups_ids) == 0: # fallback to index retrieval groups_ids = np.where( @@ -551,7 +518,7 @@ def warn_with_traceback(message, category, filename, lineno, file=None, line=Non import traceback traceback.print_stack() - log = file if hasattr(file, 'write') else sys.stderr + log = file if hasattr(file, 'write') else sys.stderr # noqa: F841 settings.write(warnings.formatwarning(message, category, filename, lineno, line)) @@ -597,9 +564,7 @@ def subsample( return Xsampled, rows -def subsample_n( - X: np.ndarray, n: int = 0, seed: int = 0 -) -> Tuple[np.ndarray, np.ndarray]: +def subsample_n(X: np.ndarray, n: int = 0, seed: int = 0) -> Tuple[np.ndarray, np.ndarray]: """Subsample n samples from rows of array. Parameters @@ -748,17 +713,12 @@ def __contains__(self, key): def _choose_graph(adata, obsp, neighbors_key): """Choose connectivities from neighbbors or another obsp column""" if obsp is not None and neighbors_key is not None: - raise ValueError( - 'You can\'t specify both obsp, neighbors_key. ' 'Please select only one.' - ) + raise ValueError('You can\'t specify both obsp, neighbors_key. ' 'Please select only one.') if obsp is not None: return adata.obsp[obsp] else: neighbors = NeighborsView(adata, neighbors_key) if 'connectivities' not in neighbors: - raise ValueError( - 'You need to run `pp.neighbors` first ' - 'to compute a neighborhood graph.' - ) + raise ValueError('You need to run `pp.neighbors` first ' 'to compute a neighborhood graph.') return neighbors['connectivities'] diff --git a/scanpy/cli.py b/scanpy/cli.py index 9a90cebd31..b7529d52e6 100644 --- a/scanpy/cli.py +++ b/scanpy/cli.py @@ -25,9 +25,7 @@ class _DelegatingSubparsersAction(_SubParsersAction): def __init__(self, *args, _command: str, _runargs: Dict[str, Any], **kwargs): super().__init__(*args, **kwargs) self.command = _command - self._name_parser_map = self.choices = _CommandDelegator( - _command, self, **_runargs - ) + self._name_parser_map = self.choices = _CommandDelegator(_command, self, **_runargs) class _CommandDelegator(cabc.MutableMapping): @@ -49,9 +47,7 @@ def __getitem__(self, k: str) -> ArgumentParser: if which(f'{self.command}-{k}'): return _DelegatingParser(self, k) # Only here is the command list retrieved - raise ArgumentError( - self.action, f'No command “{k}”. Choose from {set(self)}' - ) + raise ArgumentError(self.action, f'No command “{k}”. Choose from {set(self)}') def __setitem__(self, k: str, v: ArgumentParser) -> None: self.parser_map[k] = v @@ -74,8 +70,7 @@ def __hash__(self) -> int: def __eq__(self, other: Mapping[str, ArgumentParser]): if isinstance(other, _CommandDelegator): return all( - getattr(self, attr) == getattr(other, attr) - for attr in ['command', 'action', 'parser_map', 'runargs'] + getattr(self, attr) == getattr(other, attr) for attr in ['command', 'action', 'parser_map', 'runargs'] ) return self.parser_map == other @@ -103,9 +98,7 @@ def parse_known_args( args: Optional[Sequence[str]] = None, namespace: Optional[Namespace] = None, ) -> Tuple[Namespace, List[str]]: - assert ( - args is not None and namespace is None - ), 'Only use DelegatingParser as subparser' + assert args is not None and namespace is None, 'Only use DelegatingParser as subparser' return Namespace(func=partial(run, [self.prog, *args], **self.cd.runargs)), [] @@ -115,9 +108,7 @@ def _cmd_settings() -> None: print(settings) -def main( - argv: Optional[Sequence[str]] = None, *, check: bool = True, **runargs -) -> Optional[CompletedProcess]: +def main(argv: Optional[Sequence[str]] = None, *, check: bool = True, **runargs) -> Optional[CompletedProcess]: """\ Run a builtin scanpy command or a scanpy-* subcommand. @@ -125,10 +116,7 @@ def main( `~run(['scanpy', *argv], **runargs)` """ parser = ArgumentParser( - description=( - "There are a few packages providing commands. " - "Try e.g. `pip install scanpy-scripts`!" - ) + description=("There are a few packages providing commands. " "Try e.g. `pip install scanpy-scripts`!") ) parser.set_defaults(func=parser.print_help) diff --git a/scanpy/datasets/_datasets.py b/scanpy/datasets/_datasets.py index 060b00ddd3..ec909b41e6 100644 --- a/scanpy/datasets/_datasets.py +++ b/scanpy/datasets/_datasets.py @@ -136,13 +136,10 @@ def moignard15() -> AnnData: } # annotate each observation/cell adata.obs['exp_groups'] = [ - next(gname for gname in groups.keys() if sname.startswith(gname)) - for sname in adata.obs_names + next(gname for gname in groups.keys() if sname.startswith(gname)) for sname in adata.obs_names ] # fix the order and colors of names in "groups" - adata.obs['exp_groups'] = pd.Categorical( - adata.obs['exp_groups'], categories=list(groups.keys()) - ) + adata.obs['exp_groups'] = pd.Categorical(adata.obs['exp_groups'], categories=list(groups.keys())) adata.uns['exp_groups_colors'] = list(groups.values()) return adata @@ -162,10 +159,7 @@ def paul15() -> AnnData: ------- Annotated data matrix. """ - logg.warning( - 'In Scanpy 0.*, this returned logarithmized data. ' - 'Now it returns non-logarithmized data.' - ) + logg.warning('In Scanpy 0.*, this returned logarithmized data. ' 'Now it returns non-logarithmized data.') import h5py filename = settings.datasetdir / 'paul15/paul15.h5' @@ -335,9 +329,7 @@ def _download_visium_dataset( # Download spatial data tar_filename = f"{sample_id}_spatial.tar.gz" tar_pth = sample_dir / tar_filename - _utils.check_presence_download( - filename=tar_pth, backup_url=url_prefix + tar_filename - ) + _utils.check_presence_download(filename=tar_pth, backup_url=url_prefix + tar_filename) with tarfile.open(tar_pth) as f: for el in f: if not (sample_dir / el.name).exists(): @@ -411,9 +403,7 @@ def visium_sge( spaceranger_version = "1.1.0" else: spaceranger_version = "1.2.0" - _download_visium_dataset( - sample_id, spaceranger_version, download_image=include_hires_tiff - ) + _download_visium_dataset(sample_id, spaceranger_version, download_image=include_hires_tiff) if include_hires_tiff: adata = read_visium( settings.datasetdir / sample_id, diff --git a/scanpy/datasets/_ebi_expression_atlas.py b/scanpy/datasets/_ebi_expression_atlas.py index 013f0aa804..e45e73a18d 100644 --- a/scanpy/datasets/_ebi_expression_atlas.py +++ b/scanpy/datasets/_ebi_expression_atlas.py @@ -86,9 +86,7 @@ def read_expression_from_archive(archive: ZipFile) -> anndata.AnnData: return adata -def ebi_expression_atlas( - accession: str, *, filter_boring: bool = False -) -> anndata.AnnData: +def ebi_expression_atlas(accession: str, *, filter_boring: bool = False) -> anndata.AnnData: """\ Load a dataset from the `EBI Single Cell Expression Atlas `__ diff --git a/scanpy/external/exporting.py b/scanpy/external/exporting.py index a405cbb365..9a36dce538 100644 --- a/scanpy/external/exporting.py +++ b/scanpy/external/exporting.py @@ -75,25 +75,16 @@ def spring_project( embedding_method = 'X_' + embedding_method else: if embedding_method in adata.uns: - embedding_method = ( - 'X_' - + embedding_method - + '_' - + adata.uns[embedding_method]['params']['layout'] - ) + embedding_method = 'X_' + embedding_method + '_' + adata.uns[embedding_method]['params']['layout'] else: - raise ValueError( - 'Run the specified embedding method `%s` first.' % embedding_method - ) + raise ValueError('Run the specified embedding method `%s` first.' % embedding_method) coords = adata.obsm[embedding_method] # Make project directory and subplot directory (subplot has same name as project) # For now, the subplot is just all cells in adata project_dir: Path = Path(project_dir) - subplot_dir: Path = ( - project_dir.parent if subplot_name is None else project_dir / subplot_name - ) + subplot_dir: Path = project_dir.parent if subplot_name is None else project_dir / subplot_name subplot_dir.mkdir(parents=True, exist_ok=True) print(f'Writing subplot to {subplot_dir}') @@ -159,9 +150,7 @@ def spring_project( elif is_categorical(adata.obs[obs_name]): categorical_extras[obs_name] = [str(x) for x in adata.obs[obs_name]] else: - logg.warning( - f'Cell grouping {obs_name!r} is not a categorical variable' - ) + logg.warning(f'Cell grouping {obs_name!r} is not a categorical variable') if custom_color_tracks is None: for obs_name in adata.obs: if not is_categorical(adata.obs[obs_name]): @@ -175,9 +164,7 @@ def spring_project( elif not is_categorical(adata.obs[obs_name]): continuous_extras[obs_name] = np.array(adata.obs[obs_name]) else: - logg.warning( - f'Custom color track {obs_name!r} is not a continuous variable' - ) + logg.warning(f'Custom color track {obs_name!r} is not a continuous variable') # Write continuous colors continuous_extras['Uniform'] = np.zeros(E.shape[0]) @@ -191,12 +178,8 @@ def spring_project( # Write categorical data categorical_coloring_data = {} - categorical_coloring_data = _build_categ_colors( - categorical_coloring_data, categorical_extras - ) - _write_cell_groupings( - subplot_dir / 'categorical_coloring_data.json', categorical_coloring_data - ) + categorical_coloring_data = _build_categ_colors(categorical_coloring_data, categorical_extras) + _write_cell_groupings(subplot_dir / 'categorical_coloring_data.json', categorical_coloring_data) # Write graph in two formats for backwards compatibility edges = _get_edges(adata, neighbors_key) @@ -210,10 +193,7 @@ def spring_project( # Write 2-D coordinates, after adjusting to roughly match SPRING's default d3js force layout parameters coords = coords - coords.min(0)[None, :] - coords = ( - coords * (np.array([1000, 1000]) / coords.ptp(0))[None, :] - + np.array([200, -200])[None, :] - ) + coords = coords * (np.array([1000, 1000]) / coords.ptp(0))[None, :] + np.array([200, -200])[None, :] np.savetxt( subplot_dir / 'coordinates.txt', np.hstack((np.arange(E.shape[0])[:, None], coords)), @@ -343,14 +323,10 @@ def _get_color_stats_genes(color_stats, E, gene_list): for iG in range(E.shape[1]): n_nonzero = E.indptr[iG + 1] - E.indptr[iG] if n_nonzero > pctl_n: - pctls[iG] = np.percentile( - E.data[E.indptr[iG] : E.indptr[iG + 1]], 100 - 100 * pctl_n / n_nonzero - ) + pctls[iG] = np.percentile(E.data[E.indptr[iG] : E.indptr[iG + 1]], 100 - 100 * pctl_n / n_nonzero) else: pctls[iG] = 0 - color_stats[gene_list[iG]] = tuple( - map(float, (means[iG], stdevs[iG], mins[iG], maxes[iG], pctls[iG])) - ) + color_stats[gene_list[iG]] = tuple(map(float, (means[iG], stdevs[iG], mins[iG], maxes[iG], pctls[iG]))) return color_stats @@ -372,10 +348,7 @@ def _write_color_stats(filename, color_stats): def _build_categ_colors(categorical_coloring_data, cell_groupings): for k, labels in cell_groupings.items(): - label_colors = { - l: _frac_to_hex(float(i) / len(set(labels))) - for i, l in enumerate(list(set(labels))) - } + label_colors = {l: _frac_to_hex(float(i) / len(set(labels))) for i, l in enumerate(list(set(labels)))} categorical_coloring_data[k] = { 'label_colors': label_colors, 'label_list': labels, @@ -385,9 +358,7 @@ def _build_categ_colors(categorical_coloring_data, cell_groupings): def _write_cell_groupings(filename, categorical_coloring_data): with open(filename, 'w') as f: - f.write( - json.dumps(categorical_coloring_data, indent=4, sort_keys=True) - ) # .decode('utf-8')) + f.write(json.dumps(categorical_coloring_data, indent=4, sort_keys=True)) # .decode('utf-8')) def _export_PAGA_to_SPRING(adata, paga_coords, outpath): @@ -398,18 +369,14 @@ def _export_PAGA_to_SPRING(adata, paga_coords, outpath): sizes = list(adata.uns[group_key + '_sizes']) clus_labels = adata.obs[group_key].cat.codes.values - cell_groups = [ - [int(j) for j in np.nonzero(clus_labels == i)[0]] for i in range(len(names)) - ] + cell_groups = [[int(j) for j in np.nonzero(clus_labels == i)[0]] for i in range(len(names))] if group_key + '_colors' in adata.uns: colors = list(adata.uns[group_key + '_colors']) else: import scanpy.plotting.utils - scanpy.plotting.utils.add_colors_for_categorical_sample_annotation( - adata, group_key - ) + scanpy.plotting.utils.add_colors_for_categorical_sample_annotation(adata, group_key) colors = list(adata.uns[group_key + '_colors']) # retrieve edge level data @@ -431,9 +398,7 @@ def _export_PAGA_to_SPRING(adata, paga_coords, outpath): # make node list nodes = [] - for i, name, xy, color, size, cells in zip( - range(len(names)), names, coords, colors, sizes, cell_groups - ): + for i, name, xy, color, size, cells in zip(range(len(names)), names, coords, colors, sizes, cell_groups): nodes.append( { 'index': i, @@ -449,9 +414,7 @@ def _export_PAGA_to_SPRING(adata, paga_coords, outpath): links = [] for source, target, weight in zip(sources, targets, weights): if source < target and weight > min_edge_weight_save: - links.append( - {'source': int(source), 'target': int(target), 'weight': float(weight)} - ) + links.append({'source': int(source), 'target': int(target), 'weight': float(weight)}) # save data about edge weights edge_weight_meta = { @@ -564,10 +527,7 @@ def cellbrowser( try: import cellbrowser.cellbrowser as cb except ImportError: - logg.error( - "The package cellbrowser is not installed. " - "Install with 'pip install cellbrowser' and retry." - ) + logg.error("The package cellbrowser is not installed. " "Install with 'pip install cellbrowser' and retry.") raise data_dir = str(data_dir) diff --git a/scanpy/external/pl.py b/scanpy/external/pl.py index b35310591e..01fac05ed6 100644 --- a/scanpy/external/pl.py +++ b/scanpy/external/pl.py @@ -176,9 +176,7 @@ def sam( try: dt = adata.obsm[projection] except KeyError: - raise ValueError( - 'Please create a projection first using run_umap or run_tsne' - ) + raise ValueError('Please create a projection first using run_umap or run_tsne') else: dt = projection @@ -187,9 +185,7 @@ def sam( axes = plt.gca() if c is None: - axes.scatter( - dt[:, 0], dt[:, 1], s=s, linewidth=linewidth, edgecolor=edgecolor, **kwargs - ) + axes.scatter(dt[:, 0], dt[:, 1], s=s, linewidth=linewidth, edgecolor=edgecolor, **kwargs) return axes if isinstance(c, str): @@ -332,15 +328,14 @@ def scrublet_score_distribution( figsize: Optional[Tuple[float, float]] = (8, 3), ): """\ - Plot histogram of doublet scores for observed transcriptomes and simulated doublets. + Plot histogram of doublet scores for observed transcriptomes and simulated doublets. + + The histogram for simulated doublets is useful for determining the correct doublet score threshold. - The histogram for simulated doublets is useful for determining the correct doublet - score threshold. - Parameters ---------- adata - An annData object resulting from func:`~scanpy.external.scrublet`. + An annData object resulting from func:`~scanpy.external.scrublet`. scale_hist_obs Set y axis scale transformation in matplotlib for the plot of observed transcriptomes (e.g. "linear", "log", "symlog", "logit") @@ -353,9 +348,9 @@ def scrublet_score_distribution( See also -------- :func:`~scanpy.external.pp.scrublet`: Main way of running Scrublet, runs - preprocessing, doublet simulation (this function) and calling. + preprocessing, doublet simulation (this function) and calling. :func:`~scanpy.external.pp.scrublet_simulate_doublets`: Run Scrublet's doublet - simulation separately for advanced usage. + simulation separately for advanced usage. """ threshold = adata.uns['scrublet']['threshold'] diff --git a/scanpy/external/pp/_hashsolo.py b/scanpy/external/pp/_hashsolo.py index ecc3e8e9ac..ca2ab5179a 100644 --- a/scanpy/external/pp/_hashsolo.py +++ b/scanpy/external/pp/_hashsolo.py @@ -66,9 +66,7 @@ def gaussian_updates(data, mu_o, std_o): n = len(data) lam = 1 / np.var(data) if len(data) > 1 else lam_o lam_n = lam_o + n * lam - mu_n = ( - (np.mean(data) * n * lam + mu_o * lam_o) / lam_n if len(data) > 0 else mu_o - ) + mu_n = (np.mean(data) * n * lam + mu_o * lam_o) / lam_n if len(data) > 0 else mu_o return mu_n, (1 / (lam_n / (n + 1))) ** (1 / 2) eps = 1e-15 @@ -91,12 +89,8 @@ def gaussian_updates(data, mu_o, std_o): # barcodes with rank < k are considered to be noise global_signal_counts = np.ravel(data_sort[:, -1]) global_noise_counts = np.ravel(data_sort[:, :-number_of_non_noise_barcodes]) - global_mu_signal_o, global_sigma_signal_o = np.mean(global_signal_counts), np.std( - global_signal_counts - ) - global_mu_noise_o, global_sigma_noise_o = np.mean(global_noise_counts), np.std( - global_noise_counts - ) + global_mu_signal_o, global_sigma_signal_o = np.mean(global_signal_counts), np.std(global_signal_counts) + global_mu_noise_o, global_sigma_noise_o = np.mean(global_noise_counts), np.std(global_noise_counts) noise_params_dict = {} signal_params_dict = {} @@ -104,9 +98,7 @@ def gaussian_updates(data, mu_o, std_o): # for each barcode get empirical noise and signal distribution parameterization for x in np.arange(num_of_barcodes): sample_barcodes = data[:, x] - sample_barcodes_noise_idx = np.where(data_arg[:, :num_of_noise_barcodes] == x)[ - 0 - ] + sample_barcodes_noise_idx = np.where(data_arg[:, :num_of_noise_barcodes] == x)[0] sample_barcodes_signal_idx = np.where(data_arg[:, -1] == x) # get noise and signal counts @@ -114,12 +106,8 @@ def gaussian_updates(data, mu_o, std_o): signal_counts = sample_barcodes[sample_barcodes_signal_idx] # get parameters of distribution, assuming lognormal do update from global values - noise_param = gaussian_updates( - noise_counts, global_mu_noise_o, global_sigma_noise_o - ) - signal_param = gaussian_updates( - signal_counts, global_mu_signal_o, global_sigma_signal_o - ) + noise_param = gaussian_updates(noise_counts, global_mu_noise_o, global_sigma_noise_o) + signal_param = gaussian_updates(signal_counts, global_mu_signal_o, global_sigma_signal_o) noise_params_dict[x] = noise_param signal_params_dict[x] = signal_param @@ -127,9 +115,7 @@ def gaussian_updates(data, mu_o, std_o): counter = 0 # for each combination of noise and signal barcode calculate probiltiy of in silico and real cell hypotheses - for noise_sample_idx, signal_sample_idx in product( - np.arange(num_of_barcodes), np.arange(num_of_barcodes) - ): + for noise_sample_idx, signal_sample_idx in product(np.arange(num_of_barcodes), np.arange(num_of_barcodes)): signal_subset = data_arg[:, -1] == signal_sample_idx noise_subset = data_arg[:, -2] == noise_sample_idx subset = signal_subset & noise_subset @@ -182,15 +168,9 @@ def gaussian_updates(data, mu_o, std_o): + eps ) - probs_of_negative = np.sum( - [log_noise_noise_probs, log_signal_noise_probs], axis=0 - ) - probs_of_singlet = np.sum( - [log_noise_noise_probs, log_signal_signal_probs], axis=0 - ) - probs_of_doublet = np.sum( - [log_noise_signal_probs, log_signal_signal_probs], axis=0 - ) + probs_of_negative = np.sum([log_noise_noise_probs, log_signal_noise_probs], axis=0) + probs_of_singlet = np.sum([log_noise_noise_probs, log_signal_signal_probs], axis=0) + probs_of_doublet = np.sum([log_noise_signal_probs, log_signal_signal_probs], axis=0) log_probs_list = [probs_of_negative, probs_of_singlet, probs_of_doublet] # each cell and each hypothesis probability @@ -226,15 +206,11 @@ def _calculate_bayes_rule(data, priors, number_of_noise_barcodes): "log_likelihoods_for_each_hypothesis" key is a 2d np.array log likelihood of each hypothesis """ priors = np.array(priors) - log_likelihoods_for_each_hypothesis, _, _ = _calculate_log_likelihoods( - data, number_of_noise_barcodes - ) + log_likelihoods_for_each_hypothesis, _, _ = _calculate_log_likelihoods(data, number_of_noise_barcodes) probs_hypotheses = ( np.exp(log_likelihoods_for_each_hypothesis) * priors - / np.sum( - np.multiply(np.exp(log_likelihoods_for_each_hypothesis), priors), axis=1 - )[:, None] + / np.sum(np.multiply(np.exp(log_likelihoods_for_each_hypothesis), priors), axis=1)[:, None] ) most_likely_hypothesis = np.argmax(probs_hypotheses, axis=1) return { @@ -295,9 +271,7 @@ def hashsolo( >>> sce.pp.hashsolo(data, ['Hash1', 'Hash2', 'Hash3']) >>> data.obs.head() """ - print( - "Please cite HashSolo paper:\nhttps://www.cell.com/cell-systems/fulltext/S2405-4712(20)30195-2" - ) + print("Please cite HashSolo paper:\nhttps://www.cell.com/cell-systems/fulltext/S2405-4712(20)30195-2") data = adata.obs[cell_hashing_columns].values if not check_nonnegative_integers(data): @@ -326,67 +300,39 @@ def hashsolo( unique_cluster_features = np.unique(adata.obs[cluster_features]) for cluster_feature in unique_cluster_features: cluster_feature_bool_vector = adata.obs[cluster_features] == cluster_feature - posterior_dict = _calculate_bayes_rule( - data[cluster_feature_bool_vector], priors, number_of_noise_barcodes - ) - results.loc[ - cluster_feature_bool_vector, "most_likely_hypothesis" - ] = posterior_dict["most_likely_hypothesis"] - results.loc[ - cluster_feature_bool_vector, "cluster_feature" - ] = cluster_feature - results.loc[ - cluster_feature_bool_vector, "negative_hypothesis_probability" - ] = posterior_dict["probs_hypotheses"][:, 0] - results.loc[ - cluster_feature_bool_vector, "singlet_hypothesis_probability" - ] = posterior_dict["probs_hypotheses"][:, 1] - results.loc[ - cluster_feature_bool_vector, "doublet_hypothesis_probability" - ] = posterior_dict["probs_hypotheses"][:, 2] + posterior_dict = _calculate_bayes_rule(data[cluster_feature_bool_vector], priors, number_of_noise_barcodes) + results.loc[cluster_feature_bool_vector, "most_likely_hypothesis"] = posterior_dict[ + "most_likely_hypothesis" + ] + results.loc[cluster_feature_bool_vector, "cluster_feature"] = cluster_feature + results.loc[cluster_feature_bool_vector, "negative_hypothesis_probability"] = posterior_dict[ + "probs_hypotheses" + ][:, 0] + results.loc[cluster_feature_bool_vector, "singlet_hypothesis_probability"] = posterior_dict[ + "probs_hypotheses" + ][:, 1] + results.loc[cluster_feature_bool_vector, "doublet_hypothesis_probability"] = posterior_dict[ + "probs_hypotheses" + ][:, 2] else: posterior_dict = _calculate_bayes_rule(data, priors, number_of_noise_barcodes) - results.loc[:, "most_likely_hypothesis"] = posterior_dict[ - "most_likely_hypothesis" - ] + results.loc[:, "most_likely_hypothesis"] = posterior_dict["most_likely_hypothesis"] results.loc[:, "cluster_feature"] = 0 - results.loc[:, "negative_hypothesis_probability"] = posterior_dict[ - "probs_hypotheses" - ][:, 0] - results.loc[:, "singlet_hypothesis_probability"] = posterior_dict[ - "probs_hypotheses" - ][:, 1] - results.loc[:, "doublet_hypothesis_probability"] = posterior_dict[ - "probs_hypotheses" - ][:, 2] - - adata.obs["most_likely_hypothesis"] = results.loc[ - adata.obs_names, "most_likely_hypothesis" - ] + results.loc[:, "negative_hypothesis_probability"] = posterior_dict["probs_hypotheses"][:, 0] + results.loc[:, "singlet_hypothesis_probability"] = posterior_dict["probs_hypotheses"][:, 1] + results.loc[:, "doublet_hypothesis_probability"] = posterior_dict["probs_hypotheses"][:, 2] + + adata.obs["most_likely_hypothesis"] = results.loc[adata.obs_names, "most_likely_hypothesis"] adata.obs["cluster_feature"] = results.loc[adata.obs_names, "cluster_feature"] - adata.obs["negative_hypothesis_probability"] = results.loc[ - adata.obs_names, "negative_hypothesis_probability" - ] - adata.obs["singlet_hypothesis_probability"] = results.loc[ - adata.obs_names, "singlet_hypothesis_probability" - ] - adata.obs["doublet_hypothesis_probability"] = results.loc[ - adata.obs_names, "doublet_hypothesis_probability" - ] + adata.obs["negative_hypothesis_probability"] = results.loc[adata.obs_names, "negative_hypothesis_probability"] + adata.obs["singlet_hypothesis_probability"] = results.loc[adata.obs_names, "singlet_hypothesis_probability"] + adata.obs["doublet_hypothesis_probability"] = results.loc[adata.obs_names, "doublet_hypothesis_probability"] adata.obs["Classification"] = None - adata.obs.loc[ - adata.obs["most_likely_hypothesis"] == 2, "Classification" - ] = "Doublet" - adata.obs.loc[ - adata.obs["most_likely_hypothesis"] == 0, "Classification" - ] = "Negative" + adata.obs.loc[adata.obs["most_likely_hypothesis"] == 2, "Classification"] = "Doublet" + adata.obs.loc[adata.obs["most_likely_hypothesis"] == 0, "Classification"] = "Negative" all_sings = adata.obs["most_likely_hypothesis"] == 1 - singlet_sample_index = np.argmax( - adata.obs.loc[all_sings, cell_hashing_columns].values, axis=1 - ) - adata.obs.loc[all_sings, "Classification"] = adata.obs[ - cell_hashing_columns - ].columns[singlet_sample_index] + singlet_sample_index = np.argmax(adata.obs.loc[all_sings, cell_hashing_columns].values, axis=1) + adata.obs.loc[all_sings, "Classification"] = adata.obs[cell_hashing_columns].columns[singlet_sample_index] return adata if not inplace else None diff --git a/scanpy/external/pp/_magic.py b/scanpy/external/pp/_magic.py index 5690923c5b..aeb382733b 100644 --- a/scanpy/external/pp/_magic.py +++ b/scanpy/external/pp/_magic.py @@ -150,10 +150,7 @@ def magic( start = logg.info('computing MAGIC') all_or_pca = isinstance(name_list, (str, type(None))) if all_or_pca and name_list not in {"all_genes", "pca_only", None}: - raise ValueError( - "Invalid string value for `name_list`: " - "Only `'all_genes'` and `'pca_only'` are allowed." - ) + raise ValueError("Invalid string value for `name_list`: " "Only `'all_genes'` and `'pca_only'` are allowed.") if copy is None: copy = not all_or_pca elif not all_or_pca and not copy: @@ -181,11 +178,7 @@ def magic( logg.info( ' finished', time=start, - deep=( - "added\n 'X_magic', PCA on MAGIC coordinates (adata.obsm)" - if name_list == "pca_only" - else '' - ), + deep=("added\n 'X_magic', PCA on MAGIC coordinates (adata.obsm)" if name_list == "pca_only" else ''), ) # update AnnData instance if name_list == "pca_only": diff --git a/scanpy/external/pp/_mnn_correct.py b/scanpy/external/pp/_mnn_correct.py index 67ff5748ba..ff90cb05be 100644 --- a/scanpy/external/pp/_mnn_correct.py +++ b/scanpy/external/pp/_mnn_correct.py @@ -28,11 +28,7 @@ def mnn_correct( save_raw: bool = False, n_jobs: Optional[int] = None, **kwargs, -) -> Tuple[ - Union[np.ndarray, AnnData], - List[pd.DataFrame], - Optional[List[Tuple[Optional[float], int]]], -]: +) -> Tuple[Union[np.ndarray, AnnData], List[pd.DataFrame], Optional[List[Tuple[Optional[float], int]]],]: """\ Correct batch effects by matching mutual nearest neighbors [Haghverdi18]_ [Kang18]_. @@ -126,10 +122,7 @@ def mnn_correct( try: from mnnpy import mnn_correct except ImportError: - raise ImportError( - 'Please install the package mnnpy ' - '(https://github.com/chriscainx/mnnpy). ' - ) + raise ImportError('Please install the package mnnpy ' '(https://github.com/chriscainx/mnnpy). ') n_jobs = settings.n_jobs if n_jobs is None else n_jobs datas, mnn_list, angle_list = mnn_correct( diff --git a/scanpy/external/pp/_scanorama_integrate.py b/scanpy/external/pp/_scanorama_integrate.py index 9ea4d150cd..0d5a058ec1 100644 --- a/scanpy/external/pp/_scanorama_integrate.py +++ b/scanpy/external/pp/_scanorama_integrate.py @@ -113,9 +113,7 @@ def scanorama_integrate( name2idx[batch_name].append(idx) # Separate batches. - datasets_dimred = [ - adata.obsm[basis][name2idx[batch_name]] for batch_name in batch_names - ] + datasets_dimred = [adata.obsm[basis][name2idx[batch_name]] for batch_name in batch_names] # Integrate. integrated = scanorama.assemble( diff --git a/scanpy/external/pp/_scrublet.py b/scanpy/external/pp/_scrublet.py index 0daf69232b..46774706eb 100644 --- a/scanpy/external/pp/_scrublet.py +++ b/scanpy/external/pp/_scrublet.py @@ -1,5 +1,5 @@ from anndata import AnnData -from typing import Collection, Tuple, Optional, Union +from typing import Optional import numpy as np from ... import logging as logg @@ -38,7 +38,7 @@ def scrublet( and directly call functions of Scrublet(). You may also undertake your own preprocessing, simulate doublets with scanpy.external.pp.scrublet_simulate_doublets(), and run the core scrublet - function scanpy.external.pp.scrublet.scrublet(). + function scanpy.external.pp.scrublet.scrublet(). .. note:: More information and bug reports `here @@ -59,7 +59,7 @@ def scrublet( as adata. This should have been built from adata_obs after filtering genes and cells and selcting highly-variable genes. sim_doublet_ratio - Number of doublets to simulate relative to the number of observed + Number of doublets to simulate relative to the number of observed transcriptomes. expected_doublet_rate Where adata_sim not suplied, the estimated doublet rate for the @@ -71,8 +71,8 @@ def scrublet( synthetic doublets. If 1.0, each doublet is created by simply adding the UMI counts from two randomly sampled observed transcriptomes. For values less than 1, the UMI counts are added and then randomly sampled - at the specified rate. - knn_dist_metric + at the specified rate. + knn_dist_metric Distance metric used when finding nearest neighbors. For list of valid values, see the documentation for annoy (if `use_approx_neighbors` is True) or sklearn.neighbors.NearestNeighbors (if `use_approx_neighbors` @@ -88,16 +88,16 @@ def scrublet( If True, center the data such that each gene has a mean of 0. `sklearn.decomposition.PCA` will be used for dimensionality reduction. - n_prin_comps + n_prin_comps Number of principal components used to embed the transcriptomes prior - to k-nearest-neighbor graph construction. + to k-nearest-neighbor graph construction. use_approx_neighbors - Use approximate nearest neighbor method (annoy) for the KNN + Use approximate nearest neighbor method (annoy) for the KNN classifier. get_doublet_neighbor_parents If True, return (in .uns) the parent transcriptomes that generated the doublet neighbors of each observed transcriptome. This information can - be used to infer the cell states that generated a given doublet state. + be used to infer the cell states that generated a given doublet state. n_neighbors Number of neighbors used to construct the KNN graph of observed transcriptomes and simulated doublets. If ``None``, this is @@ -131,7 +131,7 @@ def scrublet( ``adata.uns['scrublet']['doublet_scores_sim']`` Doublet scores for each simulated doublet transcriptome - ``adata.uns['scrublet']['doublet_parents']`` + ``adata.uns['scrublet']['doublet_parents']`` Pairs of ``.obs_names`` used to generate each simulated doublet transcriptome @@ -141,16 +141,14 @@ def scrublet( See also -------- :func:`~scanpy.external.pp.scrublet_simulate_doublets`: Run Scrublet's doublet - simulation separately for advanced usage. + simulation separately for advanced usage. :func:`~scanpy.external.pl.scrublet_score_distribution`: Plot histogram of doublet - scores for observed transcriptomes and simulated doublets. + scores for observed transcriptomes and simulated doublets. """ try: import scrublet as sl except ImportError: - raise ImportError( - 'Please install scrublet: `pip install scrublet` or `conda install scrublet`.' - ) + raise ImportError('Please install scrublet: `pip install scrublet` or `conda install scrublet`.') if copy: adata = adata.copy() @@ -183,7 +181,7 @@ def scrublet( pp.highly_variable_genes(adata_obs, subset=True) else: logged = pp.log1p(adata_obs, copy=True) - hvg = pp.highly_variable_genes(logged) + _ = pp.highly_variable_genes(logged) adata_obs = adata_obs[:, logged.var['highly_variable']] # Simulate the doublets based on the raw expressions from the normalised @@ -255,7 +253,7 @@ def _scrublet_call_doublets( transcriptomes and simulated doublets. This is a wrapper around the core functions of `Scrublet `__ to allow for flexibility in applying Scanpy filtering operations upstream. Unless - you know what you're doing you should use the main scrublet() function. + you know what you're doing you should use the main scrublet() function. .. note:: More information and bug reports `here @@ -291,9 +289,9 @@ def _scrublet_call_doublets( reduction, unless `mean_center` is True. n_prin_comps Number of principal components used to embed the transcriptomes prior - to k-nearest-neighbor graph construction. + to k-nearest-neighbor graph construction. use_approx_neighbors - Use approximate nearest neighbor method (annoy) for the KNN + Use approximate nearest neighbor method (annoy) for the KNN classifier. knn_dist_metric Distance metric used when finding nearest neighbors. For list of @@ -301,10 +299,10 @@ def _scrublet_call_doublets( is True) or sklearn.neighbors.NearestNeighbors (if `use_approx_neighbors` is False). get_doublet_neighbor_parents - If True, return the parent transcriptomes that generated the - doublet neighbors of each observed transcriptome. This information can - be used to infer the cell states that generated a given - doublet state. + If True, return the parent transcriptomes that generated the + doublet neighbors of each observed transcriptome. This information can + be used to infer the cell states that generated a given + doublet state. threshold Doublet score threshold for calling a transcriptome a doublet. If `None`, this is set automatically by looking for the minimum between @@ -314,7 +312,7 @@ def _scrublet_call_doublets( predicted doublets in a 2-D embedding. random_state Initial state for doublet simulation and nearest neighbors. - verbose + verbose If True, print progress updates. Returns @@ -331,7 +329,7 @@ def _scrublet_call_doublets( ``adata.uns['scrublet']['doublet_scores_sim']`` Doublet scores for each simulated doublet transcriptome - ``adata.uns['scrublet']['doublet_parents']`` + ``adata.uns['scrublet']['doublet_parents']`` Pairs of ``.obs_names`` used to generate each simulated doublet transcriptome ``uns['scrublet']['parameters']`` @@ -340,9 +338,7 @@ def _scrublet_call_doublets( try: import scrublet as sl except ImportError: - raise ImportError( - 'Please install scrublet: `pip install scrublet` or `conda install scrublet`.' - ) + raise ImportError('Please install scrublet: `pip install scrublet` or `conda install scrublet`.') # Estimate n_neighbors if not provided, and create scrublet object. @@ -376,14 +372,10 @@ def _scrublet_call_doublets( if mean_center: logg.info('Embedding transcriptomes using PCA...') - sl.pipeline_pca( - scrub, n_prin_comps=n_prin_comps, random_state=scrub.random_state - ) + sl.pipeline_pca(scrub, n_prin_comps=n_prin_comps, random_state=scrub.random_state) else: logg.info('Embedding transcriptomes using Truncated SVD...') - sl.pipeline_truncated_svd( - scrub, n_prin_comps=n_prin_comps, random_state=scrub.random_state - ) + sl.pipeline_truncated_svd(scrub, n_prin_comps=n_prin_comps, random_state=scrub.random_state) # Score the doublets @@ -411,9 +403,7 @@ def _scrublet_call_doublets( 'parameters': { 'expected_doublet_rate': expected_doublet_rate, 'sim_doublet_ratio': ( - adata_sim.uns.get('scrublet', {}) - .get('parameters', {}) - .get('sim_doublet_ratio', None) + adata_sim.uns.get('scrublet', {}).get('parameters', {}).get('sim_doublet_ratio', None) ), 'n_neighbors': n_neighbors, 'random_state': random_state, @@ -421,9 +411,7 @@ def _scrublet_call_doublets( } if get_doublet_neighbor_parents: - adata_obs.uns['scrublet'][ - 'doublet_neighbor_parents' - ] = scrub.doublet_neighbor_parents_ + adata_obs.uns['scrublet']['doublet_neighbor_parents'] = scrub.doublet_neighbor_parents_ return adata_obs @@ -444,16 +432,16 @@ def scrublet_simulate_doublets( The annotated data matrix of shape ``n_obs`` × ``n_vars``. Rows correspond to cells and columns to genes. Genes should have been filtered for expression and variability, and the object should contain - raw expression of the same dimensions. + raw expression of the same dimensions. layer - Layer of adata where raw values are stored, or 'X' if values are in .X. + Layer of adata where raw values are stored, or 'X' if values are in .X. sim_doublet_ratio - Number of doublets to simulate relative to the number of observed + Number of doublets to simulate relative to the number of observed transcriptomes. If `None`, self.sim_doublet_ratio is used. synthetic_doublet_umi_subsampling - Rate for sampling UMIs when creating synthetic doublets. If 1.0, - each doublet is created by simply adding the UMIs from two randomly - sampled observed transcriptomes. For values less than 1, the + Rate for sampling UMIs when creating synthetic doublets. If 1.0, + each doublet is created by simply adding the UMIs from two randomly + sampled observed transcriptomes. For values less than 1, the UMI counts are added and then randomly sampled at the specified rate. @@ -462,7 +450,7 @@ def scrublet_simulate_doublets( adata : anndata.AnnData with simulated doublets in .X if ``copy=True`` it returns or else adds fields to ``adata``: - ``adata.uns['scrublet']['doublet_parents']`` + ``adata.uns['scrublet']['doublet_parents']`` Pairs of ``.obs_names`` used to generate each simulated doublet transcriptome ``uns['scrublet']['parameters']`` @@ -471,16 +459,14 @@ def scrublet_simulate_doublets( See also -------- :func:`~scanpy.external.pp.scrublet`: Main way of running Scrublet, runs - preprocessing, doublet simulation (this function) and calling. + preprocessing, doublet simulation (this function) and calling. :func:`~scanpy.external.pl.scrublet_score_distribution`: Plot histogram of doublet - scores for observed transcriptomes and simulated doublets. + scores for observed transcriptomes and simulated doublets. """ try: import scrublet as sl except ImportError: - raise ImportError( - 'Please install scrublet: `pip install scrublet` or `conda install scrublet`.' - ) + raise ImportError('Please install scrublet: `pip install scrublet` or `conda install scrublet`.') X = _get_obs_rep(adata, layer=layer) scrub = sl.Scrublet(X) diff --git a/scanpy/external/pp/_scvi.py b/scanpy/external/pp/_scvi.py index baf47e684d..9d963a4760 100644 --- a/scanpy/external/pp/_scvi.py +++ b/scanpy/external/pp/_scvi.py @@ -33,12 +33,12 @@ def scvi( Fits scVI model onto raw count data given an anndata object - scVI uses stochastic optimization and deep neural networks to aggregate information + scVI uses stochastic optimization and deep neural networks to aggregate information across similar cells and genes and to approximate the distributions that underlie observed expression values, while accounting for batch effects and limited sensitivity. To use a linear-decoded Variational AutoEncoder model (implementation of [Svensson20]_.), - set linear_decoded = True. Compared to standard VAE, this model is less powerful, but can + set linear_decoded = True. Compared to standard VAE, this model is less powerful, but can be used to inspect which genes contribute to variation in the dataset. It may also be used for all scVI tasks, like differential expression, batch correction, imputation, etc. However, batch correction may be less powerful as it assumes a linear model. @@ -69,13 +69,13 @@ def scvi( train_size The train size, either a float between 0 and 1 or an integer for the number of training samples to use batch_key - Column name in anndata.obs for batches. + Column name in anndata.obs for batches. If None, no batch correction is performed If not None, batch correction is performed per batch category use_highly_variable_genes If true, uses only the genes in anndata.var["highly_variable"] subset_genes - Optional list of indices or gene names to subset anndata. + Optional list of indices or gene names to subset anndata. If not None, use_highly_variable_genes is ignored linear_decoder If true, uses LDVAE model, which is an implementation of [Svensson20]_. @@ -89,18 +89,18 @@ def scvi( Extra arguments for UnsupervisedTrainer model_kwargs Extra arguments for VAE or LDVAE model - + Returns ------- If `copy` is true, anndata is returned. If `return_posterior` is true, the posterior object is returned - If both `copy` and `return_posterior` are true, - a tuple of anndata and the posterior are returned in that order. + If both `copy` and `return_posterior` are true, + a tuple of anndata and the posterior are returned in that order. `adata.obsm['X_scvi']` stores the latent representations `adata.obsm['X_scvi_denoised']` stores the normalized mean of the negative binomial `adata.obsm['X_scvi_sample_rate']` stores the mean of the negative binomial - + If linear_decoder is true: `adata.uns['ldvae_loadings']` stores the per-gene weights in the linear decoder as a genes by n_latent matrix. @@ -117,9 +117,7 @@ def scvi( from scvi.inference import UnsupervisedTrainer from scvi.dataset import AnnDatasetFromAnnData except ImportError: - raise ImportError( - "Please install scvi package from https://github.com/YosefLab/scVI" - ) + raise ImportError("Please install scvi package from https://github.com/YosefLab/scVI") # check if observations are unnormalized using first 10 # code from: https://github.com/theislab/dca/blob/89eee4ed01dd969b3d46e0c815382806fbfc2526/dca/io.py#L63-L69 @@ -127,9 +125,7 @@ def scvi( X_subset = adata.X[:10] else: X_subset = adata.X - norm_error = ( - 'Make sure that the dataset (adata.X) contains unnormalized count data.' - ) + norm_error = 'Make sure that the dataset (adata.X) contains unnormalized count data.' if sp.sparse.issparse(X_subset): assert (X_subset.astype(int) != X_subset).nnz == 0, norm_error else: @@ -185,9 +181,7 @@ def scvi( trainer.train(n_epochs=n_epochs, lr=lr) - full = trainer.create_posterior( - trainer.model, dataset, indices=np.arange(len(dataset)) - ) + full = trainer.create_posterior(trainer.model, dataset, indices=np.arange(len(dataset))) latent, batch_indices, labels = full.sequential().get_latent() if copy: diff --git a/scanpy/external/tl/_palantir.py b/scanpy/external/tl/_palantir.py index ba437d5f52..8fc05de277 100644 --- a/scanpy/external/tl/_palantir.py +++ b/scanpy/external/tl/_palantir.py @@ -217,9 +217,7 @@ def palantir( # MAGIC imputation if impute_data: - imp_df = run_magic_imputation( - data=adata.to_df(), dm_res=dm_res, n_steps=n_steps - ) + imp_df = run_magic_imputation(data=adata.to_df(), dm_res=dm_res, n_steps=n_steps) adata.layers['palantir_imp'] = imp_df ( diff --git a/scanpy/external/tl/_phate.py b/scanpy/external/tl/_phate.py index 0e077b429e..f4e83d65fe 100644 --- a/scanpy/external/tl/_phate.py +++ b/scanpy/external/tl/_phate.py @@ -130,8 +130,7 @@ def phate( import phate except ImportError: raise ImportError( - 'You need to install the package `phate`: please run `pip install ' - '--user phate` in a terminal.' + 'You need to install the package `phate`: please run `pip install ' '--user phate` in a terminal.' ) X_phate = phate.PHATE( n_components=n_components, diff --git a/scanpy/external/tl/_phenograph.py b/scanpy/external/tl/_phenograph.py index 2a956518a2..ef25c19e20 100644 --- a/scanpy/external/tl/_phenograph.py +++ b/scanpy/external/tl/_phenograph.py @@ -195,10 +195,7 @@ def phenograph( assert phenograph.__version__ >= "1.5.3" except (ImportError, AssertionError, AttributeError): - raise ImportError( - "please install the latest release of phenograph:\n\t" - "pip install -U PhenoGraph" - ) + raise ImportError("please install the latest release of phenograph:\n\t" "pip install -U PhenoGraph") if isinstance(adata, AnnData): try: @@ -209,11 +206,7 @@ def phenograph( data = adata copy = True - comm_key = ( - "pheno_{}".format(clustering_algo) - if clustering_algo in ["louvain", "leiden"] - else '' - ) + comm_key = "pheno_{}".format(clustering_algo) if clustering_algo in ["louvain", "leiden"] else '' ig_key = "pheno_{}_ig".format("jaccard" if jaccard else "gaussian") q_key = "pheno_{}_q".format("jaccard" if jaccard else "gaussian") diff --git a/scanpy/external/tl/_trimap.py b/scanpy/external/tl/_trimap.py index 5935d81f5a..89d636e87e 100644 --- a/scanpy/external/tl/_trimap.py +++ b/scanpy/external/tl/_trimap.py @@ -76,7 +76,7 @@ def trimap( Example ------- - + >>> import scanpy as sc >>> import scanpy.external as sce >>> pbmc = sc.datasets.pbmc68k_reduced() @@ -101,8 +101,7 @@ def trimap( X = adata.X if scp.issparse(X): raise ValueError( - 'trimap currently does not support sparse matrices. Please' - 'use a dense matrix or apply pca first.' + 'trimap currently does not support sparse matrices. Please' 'use a dense matrix or apply pca first.' ) logg.warning('`X_pca` not found. Run `sc.pp.pca` first for speedup.') X_trimap = TRIMAP( diff --git a/scanpy/external/tl/_wishbone.py b/scanpy/external/tl/_wishbone.py index 681950a264..c8f3415b58 100644 --- a/scanpy/external/tl/_wishbone.py +++ b/scanpy/external/tl/_wishbone.py @@ -93,24 +93,16 @@ def wishbone( try: from wishbone.core import wishbone as c_wishbone except ImportError: - raise ImportError( - "\nplease install wishbone:\n\n\thttps://github.com/dpeerlab/wishbone" - ) + raise ImportError("\nplease install wishbone:\n\n\thttps://github.com/dpeerlab/wishbone") # Start cell index s = np.where(adata.obs_names == start_cell)[0] if len(s) == 0: - raise RuntimeError( - f"Start cell {start_cell} not found in data. " - "Please rerun with correct start cell." - ) + raise RuntimeError(f"Start cell {start_cell} not found in data. " "Please rerun with correct start cell.") if isinstance(num_waypoints, cabc.Collection): diff = np.setdiff1d(num_waypoints, adata.obs.index) if diff.size > 0: - logging.warning( - "Some of the specified waypoints are not in the data. " - "These will be removed" - ) + logging.warning("Some of the specified waypoints are not in the data. " "These will be removed") num_waypoints = diff.tolist() elif num_waypoints > adata.shape[0]: raise RuntimeError( @@ -132,9 +124,7 @@ def wishbone( # Assign results trajectory = res["Trajectory"] - trajectory = (trajectory - np.min(trajectory)) / ( - np.max(trajectory) - np.min(trajectory) - ) + trajectory = (trajectory - np.min(trajectory)) / (np.max(trajectory) - np.min(trajectory)) adata.obs['trajectory_wishbone'] = np.asarray(trajectory) # branch_ = None @@ -147,9 +137,7 @@ def _anndata_to_wishbone(adata: AnnData): from wishbone.wb import SCData, Wishbone scdata = SCData(adata.to_df()) - scdata.diffusion_eigenvectors = pd.DataFrame( - adata.obsm['X_diffmap'], index=adata.obs_names - ) + scdata.diffusion_eigenvectors = pd.DataFrame(adata.obsm['X_diffmap'], index=adata.obs_names) wb = Wishbone(scdata) wb.trajectory = adata.obs["trajectory_wishbone"] wb.branch = adata.obs["branch_wishbone"] diff --git a/scanpy/get/get.py b/scanpy/get/get.py index 45c081687e..5f1f818dd7 100644 --- a/scanpy/get/get.py +++ b/scanpy/get/get.py @@ -96,7 +96,7 @@ def rank_genes_groups_df( def _check_indices( dim_df: pd.DataFrame, alt_index: pd.Index, - dim: "Literal['obs', 'var']", + dim: "Literal['obs', 'var']", # noqa: F821 keys: List[str], alias_index: Optional[pd.Index] = None, use_raw: bool = False, @@ -147,26 +147,21 @@ def _check_indices( if key in dim_df.columns: col_keys.append(key) if key in alt_names.index: - raise KeyError( - f"The key '{key}' is found in both adata.{dim} and {alt_repr}.{alt_search_repr}." - ) + raise KeyError(f"The key '{key}' is found in both adata.{dim} and {alt_repr}.{alt_search_repr}.") elif key in alt_names.index: val = alt_names[key] if isinstance(val, pd.Series): # while var_names must be unique, adata.var[gene_symbols] does not # It's still ambiguous to refer to a duplicated entry though. assert alias_index is not None - raise KeyError( - f"Found duplicate entries for '{key}' in {alt_repr}.{alt_search_repr}." - ) + raise KeyError(f"Found duplicate entries for '{key}' in {alt_repr}.{alt_search_repr}.") index_keys.append(val) index_aliases.append(key) else: not_found.append(key) if len(not_found) > 0: raise KeyError( - f"Could not find keys '{not_found}' in columns of `adata.{dim}` or in" - f" {alt_repr}.{alt_search_repr}." + f"Could not find keys '{not_found}' in columns of `adata.{dim}` or in" f" {alt_repr}.{alt_search_repr}." ) return col_keys, index_keys, index_aliases @@ -176,7 +171,7 @@ def _get_array_values( X, dim_names: pd.Index, keys: List[str], - axis: "Literal[0, 1]", + axis: "Literal[0, 1]", # noqa: F821 backed: bool, ): # TODO: This should be made easier on the anndata side @@ -258,9 +253,7 @@ def obs_df( >>> mean, var = grouped.mean(), grouped.var() """ if use_raw: - assert ( - layer is None - ), "Cannot specify use_raw=True and a layer at the same time." + assert layer is None, "Cannot specify use_raw=True and a layer at the same time." var = adata.raw.var else: var = adata.var @@ -407,8 +400,7 @@ def _get_obs_rep(adata, *, use_raw=False, layer=None, obsm=None, obsp=None): return adata.obsp[obsp] else: assert False, ( - "That was unexpected. Please report this bug at:\n\n\t" - " https://github.com/theislab/scanpy/issues" + "That was unexpected. Please report this bug at:\n\n\t" " https://github.com/theislab/scanpy/issues" ) @@ -434,6 +426,5 @@ def _set_obs_rep(adata, val, *, use_raw=False, layer=None, obsm=None, obsp=None) adata.obsp[obsp] = val else: assert False, ( - "That was unexpected. Please report this bug at:\n\n\t" - " https://github.com/theislab/scanpy/issues" + "That was unexpected. Please report this bug at:\n\n\t" " https://github.com/theislab/scanpy/issues" ) diff --git a/scanpy/logging.py b/scanpy/logging.py index 5226817846..ab4b522a12 100644 --- a/scanpy/logging.py +++ b/scanpy/logging.py @@ -84,9 +84,7 @@ def _set_log_level(settings, level: int): class _LogFormatter(logging.Formatter): - def __init__( - self, fmt='{levelname}: {message}', datefmt='%Y-%m-%d %H:%M', style='{' - ): + def __init__(self, fmt='{levelname}: {message}', datefmt='%Y-%m-%d %H:%M', style='{'): super().__init__(fmt, datefmt, style) def format(self, record: logging.LogRecord): @@ -100,13 +98,9 @@ def format(self, record: logging.LogRecord): if record.time_passed: # strip microseconds if record.time_passed.microseconds: - record.time_passed = timedelta( - seconds=int(record.time_passed.total_seconds()) - ) + record.time_passed = timedelta(seconds=int(record.time_passed.total_seconds())) if '{time_passed}' in record.msg: - record.msg = record.msg.replace( - '{time_passed}', str(record.time_passed) - ) + record.msg = record.msg.replace('{time_passed}', str(record.time_passed)) else: self._style._fmt += ' ({time_passed})' if record.deep: diff --git a/scanpy/neighbors/__init__.py b/scanpy/neighbors/__init__.py index 3e6d09c465..26152bbfb5 100644 --- a/scanpy/neighbors/__init__.py +++ b/scanpy/neighbors/__init__.py @@ -15,19 +15,13 @@ from ..tools._utils import _choose_representation, doc_use_rep, doc_n_pcs from .. import settings - N_DCS = 15 # default number of diffusion components -N_PCS = ( - settings.N_PCS -) # Backwards compat, constants should be defined in only one place. - +N_PCS = settings.N_PCS # Backwards compat, constants should be defined in only one place. _Method = Literal['umap', 'gauss', 'rapids'] _MetricFn = Callable[[np.ndarray, np.ndarray], float] # from sklearn.metrics.pairwise_distances.__doc__: -_MetricSparseCapable = Literal[ - 'cityblock', 'cosine', 'euclidean', 'l1', 'l2', 'manhattan' -] +_MetricSparseCapable = Literal['cityblock', 'cosine', 'euclidean', 'l1', 'l2', 'manhattan'] _MetricScipySpatial = Literal[ 'braycurtis', 'canberra', @@ -334,9 +328,7 @@ def compute_neighbors_rapids(X: np.ndarray, n_neighbors: int): return knn_indices, np.sqrt(knn_distsq) # cuml uses sqeuclidean metric so take sqrt -def _get_sparse_matrix_from_indices_distances_umap( - knn_indices, knn_dists, n_obs, n_neighbors -): +def _get_sparse_matrix_from_indices_distances_umap(knn_indices, knn_dists, n_obs, n_neighbors): rows = np.zeros((n_obs * n_neighbors), dtype=np.int64) cols = np.zeros((n_obs * n_neighbors), dtype=np.int64) vals = np.zeros((n_obs * n_neighbors), dtype=np.float64) @@ -398,16 +390,12 @@ def _compute_connectivities_umap( # In umap-learn 0.4, this returns (result, sigmas, rhos) connectivities = connectivities[0] - distances = _get_sparse_matrix_from_indices_distances_umap( - knn_indices, knn_dists, n_obs, n_neighbors - ) + distances = _get_sparse_matrix_from_indices_distances_umap(knn_indices, knn_dists, n_obs, n_neighbors) return distances, connectivities.tocsr() -def _get_sparse_matrix_from_indices_distances_numpy( - indices, distances, n_obs, n_neighbors -): +def _get_sparse_matrix_from_indices_distances_numpy(indices, distances, n_obs, n_neighbors): n_nonzero = n_obs * n_neighbors indptr = np.arange(0, n_nonzero + 1, n_neighbors) D = csr_matrix( @@ -436,9 +424,7 @@ def _get_indices_distances_from_sparse_matrix(D, n_neighbors: int): if len(neighbors[1]) > n_neighbors_m1: sorted_indices = np.argsort(D[i][neighbors].A1)[:n_neighbors_m1] indices[i, 1:] = neighbors[1][sorted_indices] - distances[i, 1:] = D[i][ - neighbors[0][sorted_indices], neighbors[1][sorted_indices] - ] + distances[i, 1:] = D[i][neighbors[0][sorted_indices], neighbors[1][sorted_indices]] else: indices[i, 1:] = neighbors[1] distances[i, 1:] = D[i][neighbors] @@ -472,9 +458,7 @@ def _make_forest_dict(forest): props = ('hyperplanes', 'offsets', 'children', 'indices') for prop in props: d[prop] = {} - sizes = np.fromiter( - (getattr(tree, prop).shape[0] for tree in forest), dtype=int - ) + sizes = np.fromiter((getattr(tree, prop).shape[0] for tree in forest), dtype=int) d[prop]['start'] = np.zeros_like(sizes) if prop == 'offsets': dims = sizes.sum() @@ -602,15 +586,9 @@ def count_nonzero(a: Union[np.ndarray, csr_matrix]) -> int: # estimating n_neighbors if self._connectivities is None: - self.n_neighbors = int( - count_nonzero(self._distances) / self._distances.shape[0] - ) + self.n_neighbors = int(count_nonzero(self._distances) / self._distances.shape[0]) else: - self.n_neighbors = int( - count_nonzero(self._connectivities) - / self._connectivities.shape[0] - / 2 - ) + self.n_neighbors = int(count_nonzero(self._connectivities) / self._connectivities.shape[0] / 2) info_str += '`.distances` `.connectivities` ' self._number_connected_components = 1 if issparse(self._connectivities): @@ -625,9 +603,7 @@ def count_nonzero(a: Union[np.ndarray, csr_matrix]) -> int: if n_dcs > len(self._eigen_values): raise ValueError( 'Cannot instantiate using `n_dcs`={}. ' - 'Compute diffmap/spectrum with more components first.'.format( - n_dcs - ) + 'Compute diffmap/spectrum with more components first.'.format(n_dcs) ) self._eigen_values = self._eigen_values[:n_dcs] self._eigen_basis = self._eigen_basis[:, :n_dcs] @@ -752,9 +728,7 @@ def compute_neighbors( if method == 'umap' and not knn: raise ValueError('`method = \'umap\' only with `knn = True`.') if method == 'rapids' and metric != 'euclidean': - raise ValueError( - "`method` 'rapids' only supports the 'euclidean' `metric`." - ) + raise ValueError("`method` 'rapids' only supports the 'euclidean' `metric`.") if method not in {'umap', 'gauss', 'rapids'}: raise ValueError("`method` needs to be 'umap', 'gauss', or 'rapids'.") if self._adata.shape[0] >= 10000 and not knn: @@ -765,14 +739,10 @@ def compute_neighbors( self.knn = knn X = _choose_representation(self._adata, use_rep=use_rep, n_pcs=n_pcs) # neighbor search - use_dense_distances = ( - metric == 'euclidean' and X.shape[0] < 8192 - ) or knn == False + use_dense_distances = (metric == 'euclidean' and X.shape[0] < 8192) or not knn if use_dense_distances: _distances = pairwise_distances(X, metric=metric, **metric_kwds) - knn_indices, knn_distances = _get_indices_distances_from_dense_matrix( - _distances, n_neighbors - ) + knn_indices, knn_distances = _get_indices_distances_from_dense_matrix(_distances, n_neighbors) if knn: self._distances = _get_sparse_matrix_from_indices_distances_numpy( knn_indices, knn_distances, X.shape[0], n_neighbors @@ -793,7 +763,7 @@ def compute_neighbors( try: if forest: self._rp_forest = _make_forest_dict(forest) - except: + except Exception: pass # write indices as attributes if write_knn_indices: @@ -825,14 +795,10 @@ def _compute_connectivities_diffmap(self, density_normalize=True): # init distances if self.knn: Dsq = self._distances.power(2) - indices, distances_sq = _get_indices_distances_from_sparse_matrix( - Dsq, self.n_neighbors - ) + indices, distances_sq = _get_indices_distances_from_sparse_matrix(Dsq, self.n_neighbors) else: Dsq = np.power(self._distances, 2) - indices, distances_sq = _get_indices_distances_from_dense_matrix( - Dsq, self.n_neighbors - ) + indices, distances_sq = _get_indices_distances_from_dense_matrix(Dsq, self.n_neighbors) # exclude the first point, the 0th neighbor indices = indices[:, 1:] @@ -858,7 +824,7 @@ def _compute_connectivities_diffmap(self, density_normalize=True): # make the weight matrix sparse if not self.knn: mask = W > 1e-14 - W[mask == False] = 0 + W[not mask] = 0 else: # restrict number of neighbors to ~k # build a symmetric mask @@ -870,11 +836,9 @@ def _compute_connectivities_diffmap(self, density_normalize=True): W[j, i] = W[i, j] mask[j, i] = True # set all entries that are not nearest neighbors to zero - W[mask == False] = 0 + W[not mask] = 0 else: - W = ( - Dsq.copy() - ) # need to copy the distance matrix here; what follows is inplace + W = Dsq.copy() # need to copy the distance matrix here; what follows is inplace for i in range(len(Dsq.indptr[:-1])): row = Dsq.indices[Dsq.indptr[i] : Dsq.indptr[i + 1]] num = 2 * sigmas[i] * sigmas[row] @@ -976,17 +940,12 @@ def compute_eigen( which = 'LM' if sort == 'decrease' else 'SM' # it pays off to increase the stability with a bit more precision matrix = matrix.astype(np.float64) - evals, evecs = scipy.sparse.linalg.eigsh( - matrix, k=n_comps, which=which, ncv=ncv - ) + evals, evecs = scipy.sparse.linalg.eigsh(matrix, k=n_comps, which=which, ncv=ncv) evals, evecs = evals.astype(np.float32), evecs.astype(np.float32) if sort == 'decrease': evals = evals[::-1] evecs = evecs[:, ::-1] - logg.info( - ' eigenvalues of transition matrix\n' - ' {}'.format(str(evals).replace('\n', '\n ')) - ) + logg.info(' eigenvalues of transition matrix\n' ' {}'.format(str(evals).replace('\n', '\n '))) if self._number_connected_components > len(evals) / 2: logg.warning('Transition matrix has many disconnected components!') self._eigen_values = evals @@ -1020,15 +979,11 @@ def _get_dpt_row(self, i): label = self._connected_components[1][i] mask = self._connected_components[1] == label row = sum( - ( - self.eigen_values[l] - / (1 - self.eigen_values[l]) - * (self.eigen_basis[i, l] - self.eigen_basis[:, l]) - ) - ** 2 + (self.eigen_values[i] / (1 - self.eigen_values[i]) * (self.eigen_basis[i, i] - self.eigen_basis[:, i])) + ** 2 # noqa: E126 # account for float32 precision - for l in range(0, self.eigen_values.size) - if self.eigen_values[l] < 0.9994 + for i in range(0, self.eigen_values.size) + if self.eigen_values[i] < 0.9994 ) # thanks to Marius Lange for pointing Alex to this: # we will likely remove the contributions from the stationary state below when making @@ -1036,9 +991,9 @@ def _get_dpt_row(self, i): # they never seem to have deteriorated results, but also other distance measures (see e.g. # PAGA paper) don't have it, which makes sense row += sum( - (self.eigen_basis[i, l] - self.eigen_basis[:, l]) ** 2 - for l in range(0, self.eigen_values.size) - if self.eigen_values[l] >= 0.9994 + (self.eigen_basis[i, j] - self.eigen_basis[:, j]) ** 2 + for j in range(0, self.eigen_values.size) + if self.eigen_values[j] >= 0.9994 ) if mask is not None: row[~mask] = np.inf @@ -1062,9 +1017,7 @@ def _set_iroot_via_xroot(self, xroot): condition, only relevant for computing pseudotime. """ if self._adata.shape[1] != xroot.size: - raise ValueError( - 'The root vector you provided does not have the ' 'correct dimension.' - ) + raise ValueError('The root vector you provided does not have the ' 'correct dimension.') # this is the squared distance dsqroot = 1e10 iroot = 0 diff --git a/scanpy/plotting/__init__.py b/scanpy/plotting/__init__.py index 4ce775e44f..ef3aaecede 100644 --- a/scanpy/plotting/__init__.py +++ b/scanpy/plotting/__init__.py @@ -77,7 +77,7 @@ Classes ------- -These classes allow fine tuning of visual parameters. +These classes allow fine tuning of visual parameters. .. autosummary:: :toctree: . diff --git a/scanpy/plotting/_anndata.py b/scanpy/plotting/_anndata.py index cf367994f7..a60711b5cc 100755 --- a/scanpy/plotting/_anndata.py +++ b/scanpy/plotting/_anndata.py @@ -139,10 +139,7 @@ def scatter( # store .uns annotations that were added to the new adata object adata.uns = adata_T.uns return axs - raise ValueError( - '`x`, `y`, and potential `color` inputs must all ' - 'come from either `.obs` or `.var`' - ) + raise ValueError('`x`, `y`, and potential `color` inputs must all ' 'come from either `.obs` or `.var`') def _scatter_obs( @@ -180,43 +177,29 @@ def _scatter_obs( use_raw = _check_use_raw(adata, use_raw) # Process layers - if layers in ['X', None] or ( - isinstance(layers, str) and layers in adata.layers.keys() - ): + if layers in ['X', None] or (isinstance(layers, str) and layers in adata.layers.keys()): layers = (layers, layers, layers) elif isinstance(layers, cabc.Collection) and len(layers) == 3: layers = tuple(layers) for layer in layers: if layer not in adata.layers.keys() and layer not in ['X', None]: - raise ValueError( - '`layers` should have elements that are ' - 'either None or in adata.layers.keys().' - ) + raise ValueError('`layers` should have elements that are ' 'either None or in adata.layers.keys().') else: raise ValueError( - "`layers` should be a string or a collection of strings " - f"with length 3, had value '{layers}'" + "`layers` should be a string or a collection of strings " f"with length 3, had value '{layers}'" ) if use_raw and layers not in [('X', 'X', 'X'), (None, None, None)]: ValueError('`use_raw` must be `False` if layers are used.') if legend_loc not in VALID_LEGENDLOCS: - raise ValueError( - f'Invalid `legend_loc`, need to be one of: {VALID_LEGENDLOCS}.' - ) + raise ValueError(f'Invalid `legend_loc`, need to be one of: {VALID_LEGENDLOCS}.') if components is None: components = '1,2' if '2d' in projection else '1,2,3' if isinstance(components, str): components = components.split(',') components = np.array(components).astype(int) - 1 # color can be a obs column name or a matplotlib color specification - keys = ( - ['grey'] - if color is None - else [color] - if isinstance(color, str) or is_color_like(color) - else color - ) + keys = ['grey'] if color is None else [color] if isinstance(color, str) or is_color_like(color) else color if title is not None and isinstance(title, str): title = [title] highlights = adata.uns['highlights'] if 'highlights' in adata.uns else [] @@ -230,9 +213,7 @@ def _scatter_obs( if basis == 'diffmap': components -= 1 except KeyError: - raise KeyError( - f'compute coordinates using visualization tool {basis} first' - ) + raise KeyError(f'compute coordinates using visualization tool {basis} first') elif x is not None and y is not None: if use_raw: if x in adata.obs.columns: @@ -332,9 +313,7 @@ def _scatter_obs( if legend_loc == 'right margin': right_margin = 0.5 if title is None and keys[0] is not None: - title = [ - key.replace('_', ' ') if not is_color_like(key) else '' for key in keys - ] + title = [key.replace('_', ' ') if not is_color_like(key) else '' for key in keys] axs = scatter_base( Y, @@ -394,13 +373,10 @@ def add_centroid(centroids, name, Y, mask): for name in groups: if name not in set(adata.obs[key].cat.categories): raise ValueError( - f'{name!r} is invalid! specify valid name, ' - f'one of {adata.obs[key].cat.categories}' + f'{name!r} is invalid! specify valid name, ' f'one of {adata.obs[key].cat.categories}' ) else: - iname = np.flatnonzero( - adata.obs[key].cat.categories.values == name - )[0] + iname = np.flatnonzero(adata.obs[key].cat.categories.values == name)[0] mask = scatter_group( axs[ikey], key, @@ -431,9 +407,7 @@ def add_centroid(centroids, name, Y, mask): if legend_fontweight is None: legend_fontweight = 'bold' if legend_fontoutline is not None: - path_effect = [ - patheffects.withStroke(linewidth=legend_fontoutline, foreground='w') - ] + path_effect = [patheffects.withStroke(linewidth=legend_fontoutline, foreground='w')] else: path_effect = None for name, pos in centroids.items(): @@ -475,9 +449,7 @@ def add_centroid(centroids, name, Y, mask): fontsize=legend_fontsize, ) elif legend_loc != 'none': - legend = axs[ikey].legend( - frameon=False, loc=legend_loc, fontsize=legend_fontsize - ) + legend = axs[ikey].legend(frameon=False, loc=legend_loc, fontsize=legend_fontsize) if legend is not None: for handle in legend.legendHandles: handle.set_sizes([300.0]) @@ -541,18 +513,14 @@ def ranking( if log: scores = np.log(scores) if labels is None: - labels = ( - adata.var_names - if attr in {'var', 'varm'} - else np.arange(scores.shape[0]).astype(str) - ) + labels = adata.var_names if attr in {'var', 'varm'} else np.arange(scores.shape[0]).astype(str) if isinstance(labels, str): labels = [labels + str(i + 1) for i in range(scores.shape[0])] if n_panels <= 5: n_rows, n_cols = 1, n_panels else: n_rows, n_cols = 2, int(n_panels / 2 + 0.5) - fig = pl.figure( + _ = pl.figure( figsize=( n_cols * rcParams['figure.figsize'][0], n_rows * rcParams['figure.figsize'][1], @@ -697,14 +665,9 @@ def violin( ylabel = [ylabel] * (1 if groupby is None else len(keys)) if groupby is None: if len(ylabel) != 1: - raise ValueError( - f'Expected number of y-labels to be `1`, found `{len(ylabel)}`.' - ) + raise ValueError(f'Expected number of y-labels to be `1`, found `{len(ylabel)}`.') elif len(ylabel) != len(keys): - raise ValueError( - f'Expected number of y-labels to be `{len(keys)}`, ' - f'found `{len(ylabel)}`.' - ) + raise ValueError(f'Expected number of y-labels to be `{len(keys)}`, ' f'found `{len(ylabel)}`.') if groupby is not None: obs_df = get.obs_df(adata, keys=[groupby] + keys, layer=layer, use_raw=use_raw) @@ -715,9 +678,7 @@ def violin( f'but is of dtype {adata.obs[groupby].dtype}.' ) _utils.add_colors_for_categorical_sample_annotation(adata, groupby) - kwds['palette'] = dict( - zip(obs_df[groupby].cat.categories, adata.uns[f'{groupby}_colors']) - ) + kwds['palette'] = dict(zip(obs_df[groupby].cat.categories, adata.uns[f'{groupby}_colors'])) else: obs_df = get.obs_df(adata, keys=keys, layer=layer, use_raw=use_raw) if groupby is None: @@ -1040,9 +1001,7 @@ def heatmap( # reorder groupby colors if groupby_colors is not None: - groupby_colors = [ - groupby_colors[x] for x in dendro_data['categories_idx_ordered'] - ] + groupby_colors = [groupby_colors[x] for x in dendro_data['categories_idx_ordered']] if show_gene_labels is None: if len(var_names) <= 50: @@ -1131,14 +1090,10 @@ def heatmap( labels, groupby_cmap, norm, - ) = _plot_categories_as_colorblocks( - groupby_ax, obs_tidy, colors=groupby_colors, orientation='left' - ) + ) = _plot_categories_as_colorblocks(groupby_ax, obs_tidy, colors=groupby_colors, orientation='left') # add lines to main heatmap - line_positions = ( - np.cumsum(obs_tidy.index.value_counts(sort=False))[:-1] - 0.5 - ) + line_positions = np.cumsum(obs_tidy.index.value_counts(sort=False))[:-1] - 0.5 heatmap_ax.hlines( line_positions, -0.5, @@ -1151,9 +1106,7 @@ def heatmap( if dendrogram: dendro_ax = fig.add_subplot(axs[1, 2], sharey=heatmap_ax) - _plot_dendrogram( - dendro_ax, adata, groupby, ticks=ticks, dendrogram_key=dendrogram - ) + _plot_dendrogram(dendro_ax, adata, groupby, ticks=ticks, dendrogram_key=dendrogram) # plot group legends on top of heatmap_ax (if given) if var_group_positions is not None and len(var_group_positions) > 0: @@ -1232,13 +1185,9 @@ def heatmap( labels, groupby_cmap, norm, - ) = _plot_categories_as_colorblocks( - groupby_ax, obs_tidy, colors=groupby_colors, orientation='bottom' - ) + ) = _plot_categories_as_colorblocks(groupby_ax, obs_tidy, colors=groupby_colors, orientation='bottom') # add lines to main heatmap - line_positions = ( - np.cumsum(obs_tidy.index.value_counts(sort=False))[:-1] - 0.5 - ) + line_positions = np.cumsum(obs_tidy.index.value_counts(sort=False))[:-1] - 0.5 heatmap_ax.vlines( line_positions, -0.5, @@ -1264,17 +1213,13 @@ def heatmap( if var_group_positions is not None and len(var_group_positions) > 0: gene_groups_ax = fig.add_subplot(axs[1, 1]) arr = [] - for idx, (label, pos) in enumerate( - zip(var_group_labels, var_group_positions) - ): + for idx, (label, pos) in enumerate(zip(var_group_labels, var_group_positions)): if var_groups_subset_of_groupby: label_code = label2code[label] else: label_code = idx arr += [label_code] * (pos[1] + 1 - pos[0]) - gene_groups_ax.imshow( - np.array([arr]).T, aspect='auto', cmap=groupby_cmap, norm=norm - ) + gene_groups_ax.imshow(np.array([arr]).T, aspect='auto', cmap=groupby_cmap, norm=norm) gene_groups_ax.axis('off') # plot colorbar @@ -1401,9 +1346,7 @@ def tracksplot( ) categories = [categories[x] for x in dendro_data['categories_idx_ordered']] - groupby_colors = [ - groupby_colors[x] for x in dendro_data['categories_idx_ordered'] - ] + groupby_colors = [groupby_colors[x] for x in dendro_data['categories_idx_ordered']] obs_tidy = obs_tidy.sort_index() @@ -1474,7 +1417,7 @@ def tracksplot( ymin, ymax = ax.get_ylim() ymax = int(ymax) ax.set_yticks([ymax]) - tt = ax.set_yticklabels([str(ymax)], ha='left', va='top') + ax.set_yticklabels([str(ymax)], ha='left', va='top') ax.spines['right'].set_position(('axes', 1.01)) ax.tick_params( axis='y', @@ -1494,9 +1437,7 @@ def tracksplot( # the ax to plot the groupby categories is split to add a small space # between the rest of the plot and the categories - axs2 = gridspec.GridSpecFromSubplotSpec( - 2, 1, subplot_spec=axs[num_rows - 1, 0], height_ratios=[1, 1] - ) + axs2 = gridspec.GridSpecFromSubplotSpec(2, 1, subplot_spec=axs[num_rows - 1, 0], height_ratios=[1, 1]) groupby_ax = fig.add_subplot(axs2[1]) @@ -1527,9 +1468,7 @@ def tracksplot( for idx, pos in enumerate(var_group_positions): arr += [idx] * (pos[1] + 1 - pos[0]) - gene_groups_ax.imshow( - np.array([arr]).T, aspect='auto', cmap=groupby_cmap, norm=norm - ) + gene_groups_ax.imshow(np.array([arr]).T, aspect='auto', cmap=groupby_cmap, norm=norm) gene_groups_ax.axis('off') return_ax_dict = {'track_axes': axs_list, 'groupby_ax': groupby_ax} @@ -1830,10 +1769,7 @@ def _prepare_dataframe( f'Given {group}, is not in observations: {adata.obs_keys()}' + msg ) if group in adata.obs.keys() and group == adata.obs.index.name: - raise ValueError( - f'Given group {group} is both and index and a column level, ' - 'which is ambiguous.' - ) + raise ValueError(f'Given group {group} is both and index and a column level, ' 'which is ambiguous.') if group == adata.obs.index.name: groupby_index = group if groupby_index is not None: @@ -1842,9 +1778,7 @@ def _prepare_dataframe( groupby = groupby.copy() # copy to not modify user passed parameter groupby.remove(groupby_index) keys = list(groupby) + list(np.unique(var_names)) - obs_tidy = get.obs_df( - adata, keys=keys, layer=layer, use_raw=use_raw, gene_symbols=gene_symbols - ) + obs_tidy = get.obs_df(adata, keys=keys, layer=layer, use_raw=use_raw, gene_symbols=gene_symbols) assert np.all(np.array(keys) == np.array(obs_tidy.columns)) if groupby_index is not None: @@ -1960,7 +1894,7 @@ def _plot_gene_groups_brackets( va='bottom', rotation=rotation, ) - except: + except Exception: pass else: top = left @@ -2005,9 +1939,7 @@ def _plot_gene_groups_brackets( # remove y ticks gene_groups_ax.tick_params(axis='y', left=False, labelleft=False) # remove x ticks and labels - gene_groups_ax.tick_params( - axis='x', bottom=False, labelbottom=False, labeltop=False - ) + gene_groups_ax.tick_params(axis='x', bottom=False, labelbottom=False, labeltop=False) def _reorder_categories_after_dendrogram( @@ -2083,9 +2015,7 @@ def _reorder_categories_after_dendrogram( position = var_group_positions[idx] _var_names = var_names[position[0] : position[1] + 1] var_names_idx_ordered.extend(range(position[0], position[1] + 1)) - positions_ordered.append( - (position_start, position_start + len(_var_names) - 1) - ) + positions_ordered.append((position_start, position_start + len(_var_names) - 1)) position_start += len(_var_names) labels_ordered.append(var_group_labels[idx]) var_group_labels = labels_ordered @@ -2143,8 +2073,7 @@ def _get_dendrogram_key(adata, dendrogram_key, groupby): if 'dendrogram_info' not in adata.uns[dendrogram_key]: raise ValueError( - f"The given dendrogram key ({dendrogram_key!r}) does not contain " - "valid dendrogram information." + f"The given dendrogram key ({dendrogram_key!r}) does not contain " "valid dendrogram information." ) return dendrogram_key @@ -2216,9 +2145,7 @@ def translate_pos(pos_list, new_ticks, old_ticks): old_max = old_ticks[idx_next] new_min = new_ticks[idx_prev] new_max = new_ticks[idx_next] - new_x_val = ((x_val - old_min) / (old_max - old_min)) * ( - new_max - new_min - ) + new_min + new_x_val = ((x_val - old_min) / (old_max - old_min)) * (new_max - new_min) + new_min new_xs.append(new_x_val) return new_xs @@ -2230,10 +2157,7 @@ def translate_pos(pos_list, new_ticks, old_ticks): orig_ticks = np.arange(5, len(leaves) * 10 + 5, 10).astype(float) # check that ticks has the same length as orig_ticks if ticks is not None and len(orig_ticks) != len(ticks): - logg.warning( - "ticks argument does not have the same size as orig_ticks. " - "The argument will be ignored" - ) + logg.warning("ticks argument does not have the same size as orig_ticks. " "The argument will be ignored") ticks = None for xs, ys in zip(icoord, dcoord): @@ -2263,9 +2187,7 @@ def translate_pos(pos_list, new_ticks, old_ticks): dendro_ax.tick_params(labeltop=True, labelbottom=False) if remove_labels: - dendro_ax.tick_params( - labelbottom=False, labeltop=False, labelleft=False, labelright=False - ) + dendro_ax.tick_params(labelbottom=False, labeltop=False, labelleft=False, labelright=False) dendro_ax.grid(False) @@ -2316,9 +2238,7 @@ def _plot_categories_as_colorblocks( ticks = [] # list of centered position of the labels labels = [] label2code = {} # dictionary of numerical values asigned to each label - for code, (label, value) in enumerate( - obs_tidy.index.value_counts(sort=False).iteritems() - ): + for code, (label, value) in enumerate(obs_tidy.index.value_counts(sort=False).iteritems()): ticks.append(value_sum + (value / 2)) labels.append(label) value_sum += value diff --git a/scanpy/plotting/_baseplot_class.py b/scanpy/plotting/_baseplot_class.py index 38a1a76da1..ada2330436 100644 --- a/scanpy/plotting/_baseplot_class.py +++ b/scanpy/plotting/_baseplot_class.py @@ -94,11 +94,7 @@ def __init__( self.var_group_rotation = var_group_rotation self.width, self.height = figsize if figsize is not None else (None, None) - self.has_var_groups = ( - True - if var_group_positions is not None and len(var_group_positions) > 0 - else False - ) + self.has_var_groups = True if var_group_positions is not None and len(var_group_positions) > 0 else False self._update_var_groups() @@ -113,15 +109,12 @@ def __init__( gene_symbols=gene_symbols, ) if len(self.categories) > self.MAX_NUM_CATEGORIES: - warn( - f"Over {self.MAX_NUM_CATEGORIES} categories found. " - "Plot would be very large." - ) + warn(f"Over {self.MAX_NUM_CATEGORIES} categories found. " "Plot would be very large.") if categories_order is not None: if set(self.obs_tidy.index.categories) != set(categories_order): logg.error( - "Please check that the categories given by " + "Please check that the categories given by " # noqa: F821 "the `order` parameter match the categories that " "want to be reordered.\n\n" "Mismatch: " @@ -247,10 +240,7 @@ def add_dendrogram( if self.groupby is None or len(self.categories) <= 2: # dendrogram can only be computed between groupby categories - logg.warning( - "Dendrogram not added. Dendrogram is added only " - "when the number of categories to plot > 2" - ) + logg.warning("Dendrogram not added. Dendrogram is added only " "when the number of categories to plot > 2") return self self.group_extra_size = size @@ -401,9 +391,7 @@ def get_axes(self): self.make_figure() return self.ax_dict - def _plot_totals( - self, total_barplot_ax: Axes, orientation: Literal['top', 'right'] - ): + def _plot_totals(self, total_barplot_ax: Axes, orientation: Literal['top', 'right']): """ Makes the bar plot for totals """ @@ -498,9 +486,7 @@ def _plot_colorbar(self, color_legend_ax: Axes, normalize): cmap = pl.get_cmap(self.cmap) import matplotlib.colorbar - matplotlib.colorbar.ColorbarBase( - color_legend_ax, orientation='horizontal', cmap=cmap, norm=normalize - ) + matplotlib.colorbar.ColorbarBase(color_legend_ax, orientation='horizontal', cmap=cmap, norm=normalize) color_legend_ax.set_title(self.color_legend_title, fontsize='small') @@ -519,9 +505,7 @@ def _plot_legend(self, legend_ax, return_ax_dict, normalize): self.height - legend_height, legend_height, ] - fig, legend_gs = make_grid_spec( - legend_ax, nrows=2, ncols=1, height_ratios=height_ratios - ) + fig, legend_gs = make_grid_spec(legend_ax, nrows=2, ncols=1, height_ratios=height_ratios) color_legend_ax = fig.add_subplot(legend_gs[1]) @@ -563,9 +547,7 @@ def _mainplot(self, ax): ax.set_ylim(len(y_labels), 0) ax.set_xlim(0, len(x_labels)) - normalize = matplotlib.colors.Normalize( - vmin=self.kwds.get('vmin'), vmax=self.kwds.get('vmax') - ) + normalize = matplotlib.colors.Normalize(vmin=self.kwds.get('vmin'), vmax=self.kwds.get('vmax')) return normalize @@ -596,9 +578,7 @@ def make_figure(self): if self.height is None: mainplot_height = len(self.categories) * category_height - mainplot_width = ( - len(self.var_names) * category_width + self.group_extra_size - ) + mainplot_width = len(self.var_names) * category_width + self.group_extra_size if self.are_axes_swapped: mainplot_height, mainplot_width = mainplot_width, mainplot_height @@ -863,9 +843,7 @@ def _format_first_three_categories(_categories): position = self.var_group_positions[idx] _var_names = self.var_names[position[0] : position[1] + 1] var_names_idx_ordered.extend(range(position[0], position[1] + 1)) - positions_ordered.append( - (position_start, position_start + len(_var_names) - 1) - ) + positions_ordered.append((position_start, position_start + len(_var_names) - 1)) position_start += len(_var_names) labels_ordered.append(self.var_group_labels[idx]) self.var_group_labels = labels_ordered @@ -1011,9 +989,7 @@ def _plot_var_groups_brackets( # remove y ticks gene_groups_ax.tick_params(axis='y', left=False, labelleft=False) # remove x ticks and labels - gene_groups_ax.tick_params( - axis='x', bottom=False, labelbottom=False, labeltop=False - ) + gene_groups_ax.tick_params(axis='x', bottom=False, labelbottom=False, labeltop=False) def _update_var_groups(self): """ diff --git a/scanpy/plotting/_dotplot.py b/scanpy/plotting/_dotplot.py index c63f0b46e5..9e7ea36355 100644 --- a/scanpy/plotting/_dotplot.py +++ b/scanpy/plotting/_dotplot.py @@ -156,16 +156,12 @@ def __init__( # of values >expression_cutoff, and divide the result by the total number of # values in the group (given by `count()`) if dot_size_df is None: - dot_size_df = ( - obs_bool.groupby(level=0).sum() / obs_bool.groupby(level=0).count() - ) + dot_size_df = obs_bool.groupby(level=0).sum() / obs_bool.groupby(level=0).count() if dot_color_df is None: # 2. compute mean expression value value if mean_only_expressed: - dot_color_df = ( - self.obs_tidy.mask(~obs_bool).groupby(level=0).mean().fillna(0) - ) + dot_color_df = self.obs_tidy.mask(~obs_bool).groupby(level=0).mean().fillna(0) else: dot_color_df = self.obs_tidy.groupby(level=0).mean() @@ -196,9 +192,7 @@ def __init__( # with df[['a', 'a', 'b']], results in a df with columns: # ['a', 'a', 'a', 'a', 'b'] - unique_var_names, unique_idx = np.unique( - dot_color_df.columns, return_index=True - ) + unique_var_names, unique_idx = np.unique(dot_color_df.columns, return_index=True) # remove duplicate columns if len(unique_var_names) != len(self.var_names): dot_color_df = dot_color_df.iloc[:, unique_idx] @@ -438,15 +432,11 @@ def _plot_size_legend(self, size_legend_ax: Axes): zorder=100, ) size_legend_ax.set_xticks(np.arange(len(size)) + 0.5) - labels = [ - "{}".format(np.round((x * 100), decimals=0).astype(int)) for x in size_range - ] + labels = ["{}".format(np.round((x * 100), decimals=0).astype(int)) for x in size_range] size_legend_ax.set_xticklabels(labels, fontsize='small') # remove y ticks and labels - size_legend_ax.tick_params( - axis='y', left=False, labelleft=False, labelright=False - ) + size_legend_ax.tick_params(axis='y', left=False, labelleft=False, labelright=False) # remove surrounding lines size_legend_ax.spines['right'].set_visible(False) @@ -483,9 +473,7 @@ def _plot_legend(self, legend_ax, return_ax_dict, normalize): spacer_height, cbar_legend_height, ] - fig, legend_gs = make_grid_spec( - legend_ax, nrows=4, ncols=1, height_ratios=height_ratios - ) + fig, legend_gs = make_grid_spec(legend_ax, nrows=4, ncols=1, height_ratios=height_ratios) if self.show_size_legend: size_legend_ax = fig.add_subplot(legend_gs[1]) @@ -631,8 +619,7 @@ def _dotplot( ) assert list(dot_size.columns) == list(dot_color.columns), ( - 'please check that the dot_size ' - 'and dot_color dataframes have the same columns' + 'please check that the dot_size ' 'and dot_color dataframes have the same columns' ) if standard_scale == 'group': @@ -685,9 +672,7 @@ def _dotplot( import matplotlib.colors - normalize = matplotlib.colors.Normalize( - vmin=kwds.get('vmin'), vmax=kwds.get('vmax') - ) + normalize = matplotlib.colors.Normalize(vmin=kwds.get('vmin'), vmax=kwds.get('vmax')) if color_on == 'square': if edge_color is None: @@ -738,9 +723,7 @@ def _dotplot( y_ticks = np.arange(dot_color.shape[0]) + 0.5 dot_ax.set_yticks(y_ticks) - dot_ax.set_yticklabels( - [dot_color.index[idx] for idx, _ in enumerate(y_ticks)], minor=False - ) + dot_ax.set_yticklabels([dot_color.index[idx] for idx, _ in enumerate(y_ticks)], minor=False) x_ticks = np.arange(dot_color.shape[1]) + 0.5 dot_ax.set_xticks(x_ticks) diff --git a/scanpy/plotting/_matrixplot.py b/scanpy/plotting/_matrixplot.py index 6a225a440c..8af68f844d 100644 --- a/scanpy/plotting/_matrixplot.py +++ b/scanpy/plotting/_matrixplot.py @@ -205,9 +205,7 @@ def _mainplot(self, ax): import matplotlib.colors - normalize = matplotlib.colors.Normalize( - vmin=self.kwds.get('vmin'), vmax=self.kwds.get('vmax') - ) + normalize = matplotlib.colors.Normalize(vmin=self.kwds.get('vmin'), vmax=self.kwds.get('vmax')) for axis in ['top', 'bottom', 'left', 'right']: ax.spines[axis].set_linewidth(1.5) @@ -219,7 +217,7 @@ def _mainplot(self, ax): linewidth=self.edge_lw, norm=normalize, ) - __ = ax.pcolor(_color_df, **kwds) + _ = ax.pcolor(_color_df, **kwds) y_labels = _color_df.index x_labels = _color_df.columns diff --git a/scanpy/plotting/_preprocessing.py b/scanpy/plotting/_preprocessing.py index 9837773334..ad9e64d27b 100644 --- a/scanpy/plotting/_preprocessing.py +++ b/scanpy/plotting/_preprocessing.py @@ -120,6 +120,4 @@ def filter_genes_dispersion( A string is appended to the default filename. Infer the filetype if ending on {{`'.pdf'`, `'.png'`, `'.svg'`}}. """ - highly_variable_genes( - result, log=log, show=show, save=save, highly_variable_genes=False - ) + highly_variable_genes(result, log=log, show=show, save=save, highly_variable_genes=False) diff --git a/scanpy/plotting/_qc.py b/scanpy/plotting/_qc.py index 3259e66425..d33d5fec2a 100644 --- a/scanpy/plotting/_qc.py +++ b/scanpy/plotting/_qc.py @@ -75,14 +75,8 @@ def highest_expr_genes( mean_percent = norm_dict['X'].mean(axis=0) top_idx = np.argsort(mean_percent)[::-1][:n_top] counts_top_genes = norm_dict['X'][:, top_idx] - columns = ( - adata.var_names[top_idx] - if gene_symbols is None - else adata.var[gene_symbols][top_idx] - ) - counts_top_genes = pd.DataFrame( - counts_top_genes, index=adata.obs_names, columns=columns - ) + columns = adata.var_names[top_idx] if gene_symbols is None else adata.var[gene_symbols][top_idx] + counts_top_genes = pd.DataFrame(counts_top_genes, index=adata.obs_names, columns=columns) if not ax: # figsize is hardcoded to produce a tall image. To change the fig size, diff --git a/scanpy/plotting/_stacked_violin.py b/scanpy/plotting/_stacked_violin.py index 16abbaeeb9..11a1f51efd 100644 --- a/scanpy/plotting/_stacked_violin.py +++ b/scanpy/plotting/_stacked_violin.py @@ -308,9 +308,7 @@ def _mainplot(self, ax): _matrix = _matrix.iloc[:, self.var_names_idx_order] if self.categories_order is not None: - _matrix.index = _matrix.index.reorder_categories( - self.categories_order, ordered=True - ) + _matrix.index = _matrix.index.reorder_categories(self.categories_order, ordered=True) # get mean values for color and transform to color values # using colormap @@ -319,18 +317,14 @@ def _mainplot(self, ax): _color_df = _color_df.T import matplotlib.colors - norm = matplotlib.colors.Normalize( - vmin=self.kwds.get('vmin'), vmax=self.kwds.get('vmax') - ) + norm = matplotlib.colors.Normalize(vmin=self.kwds.get('vmin'), vmax=self.kwds.get('vmax')) cmap = pl.get_cmap(self.kwds.get('cmap', self.cmap)) if 'cmap' in self.kwds: del self.kwds['cmap'] colormap_array = cmap(norm(_color_df.values)) x_spacer_size = self.plot_x_padding y_spacer_size = self.plot_y_padding - self._make_rows_of_violinplots( - ax, _matrix, colormap_array, _color_df, x_spacer_size, y_spacer_size - ) + self._make_rows_of_violinplots(ax, _matrix, colormap_array, _color_df, x_spacer_size, y_spacer_size) # turn on axis for `ax` as this is turned off # by make_grid_spec when the axis is subdivided earlier. @@ -345,9 +339,7 @@ def _mainplot(self, ax): # 0.5 to position the ticks on the center of the violins y_ticks = np.arange(_color_df.shape[0]) + 0.5 ax.set_yticks(y_ticks) - ax.set_yticklabels( - [_color_df.index[idx] for idx, _ in enumerate(y_ticks)], minor=False - ) + ax.set_yticklabels([_color_df.index[idx] for idx, _ in enumerate(y_ticks)], minor=False) # 0.5 to position the ticks on the center of the violins x_ticks = np.arange(_color_df.shape[1]) + 0.5 @@ -362,9 +354,7 @@ def _mainplot(self, ax): return norm - def _make_rows_of_violinplots( - self, ax, _matrix, colormap_array, _color_df, x_spacer_size, y_spacer_size - ): + def _make_rows_of_violinplots(self, ax, _matrix, colormap_array, _color_df, x_spacer_size, y_spacer_size): import seaborn as sns # Slow import, only import if called row_palette = self.kwds.get('color', self.row_palette) @@ -403,14 +393,8 @@ def _make_rows_of_violinplots( } ) ) - df['genes'] = ( - df['genes'].astype('category').cat.reorder_categories(_matrix.columns) - ) - df['categories'] = ( - df['categories'] - .astype('category') - .cat.reorder_categories(_matrix.index.categories) - ) + df['genes'] = df['genes'].astype('category').cat.reorder_categories(_matrix.columns) + df['categories'] = df['categories'].astype('category').cat.reorder_categories(_matrix.index.categories) # the ax need to be subdivided # define a layout of nrows = len(categories) rows @@ -512,9 +496,7 @@ def _setup_violin_axes_ticks(self, row_ax, num_cols): import matplotlib.ticker as ticker # use MaxNLocator to set 2 ticks - row_ax.yaxis.set_major_locator( - ticker.MaxNLocator(nbins=2, steps=[1, 1.2, 10]) - ) + row_ax.yaxis.set_major_locator(ticker.MaxNLocator(nbins=2, steps=[1, 1.2, 10])) yticks = row_ax.get_yticks() row_ax.set_yticks([yticks[0], yticks[-1]]) ticklabels = row_ax.get_yticklabels() @@ -531,9 +513,7 @@ def _setup_violin_axes_ticks(self, row_ax, num_cols): row_ax.set_xlabel('') row_ax.set_xticklabels([]) - row_ax.tick_params( - axis='x', bottom=False, top=False, labeltop=False, labelbottom=False - ) + row_ax.tick_params(axis='x', bottom=False, top=False, labeltop=False, labelbottom=False) @_doc_params( diff --git a/scanpy/plotting/_tools/__init__.py b/scanpy/plotting/_tools/__init__.py index dc5f24ad05..47e5537b69 100644 --- a/scanpy/plotting/_tools/__init__.py +++ b/scanpy/plotting/_tools/__init__.py @@ -56,7 +56,7 @@ def pca_overview(adata: AnnData, **params): show = params['show'] if 'show' in params else None if 'show' in params: del params['show'] - scatterplots.pca(adata, **params, show=False) + scatterplots.pca(adata, **params, show=False) # noqa: F821 pca_loadings(adata, show=False) pca_variance_ratio(adata, show=show) @@ -393,14 +393,10 @@ def _rank_genes_groups_plot( if min_logfoldchange is not None: df = rank_genes_groups_df(adata, group, key=key) # select genes with given log_fold change - genes_list = df[df.logfoldchanges > min_logfoldchange].names.tolist()[ - :n_genes - ] + genes_list = df[df.logfoldchanges > min_logfoldchange].names.tolist()[:n_genes] else: # get all genes that are 'non-nan' - genes_list = [ - gene for gene in adata.uns[key]['names'][group] if not pd.isnull(gene) - ][:n_genes] + genes_list = [gene for gene in adata.uns[key]['names'][group] if not pd.isnull(gene)][:n_genes] if len(genes_list) == 0: logg.warning(f'No genes found for group {group}') @@ -441,9 +437,7 @@ def _rank_genes_groups_plot( elif plot_type == 'matrixplot': from .._matrixplot import matrixplot - _pl = matrixplot( - adata, var_names, groupby, values_df=values_df, return_fig=True, **kwds - ) + _pl = matrixplot(adata, var_names, groupby, values_df=values_df, return_fig=True, **kwds) if title is not None and 'colorbar_title' not in kwds: _pl.legend(title=title) @@ -954,14 +948,10 @@ def rank_genes_groups_violin( _ax.legend_.remove() _ax.set_ylabel('expression') _ax.set_xticklabels(new_gene_names, rotation='vertical') - writekey = ( - f"rank_genes_groups_" - f"{adata.uns[key]['params']['groupby']}_" - f"{group_name}" - ) + writekey = f"rank_genes_groups_" f"{adata.uns[key]['params']['groupby']}_" f"{group_name}" savefig_or_show(writekey, show=show, save=save) axs.append(_ax) - if show == False: + if not show: return axs @@ -1147,15 +1137,11 @@ def embedding_density( if f'X_{basis}' not in adata.obsm_keys(): raise ValueError( - f'Cannot find the embedded representation `adata.obsm[X_{basis!r}]`. ' - 'Compute the embedding first.' + f'Cannot find the embedded representation `adata.obsm[X_{basis!r}]`. ' 'Compute the embedding first.' ) if key not in adata.obs or f'{key}_params' not in adata.uns: - raise ValueError( - 'Please run `sc.tl.embedding_density()` first ' - 'and specify the correct key.' - ) + raise ValueError('Please run `sc.tl.embedding_density()` first ' 'and specify the correct key.') if 'components' in kwargs: logg.warning( @@ -1178,15 +1164,11 @@ def embedding_density( if group is None and groupby is not None: raise ValueError( - 'Densities were calculated over an `.obs` covariate. ' - 'Please specify a group from this covariate to plot.' + 'Densities were calculated over an `.obs` covariate. ' 'Please specify a group from this covariate to plot.' ) if group is not None and groupby is None: - logg.warning( - "value of 'group' is ignored because densities " - "were not calculated for an `.obs` covariate." - ) + logg.warning("value of 'group' is ignored because densities " "were not calculated for an `.obs` covariate.") group = None if np.min(adata.obs[key]) < 0 or np.max(adata.obs[key]) > 1: @@ -1214,11 +1196,7 @@ def embedding_density( # if group is set, then plot it using multiple panels # (even if only one group is set) - if ( - group is not None - and not isinstance(group, str) - and isinstance(group, cabc.Sequence) - ): + if group is not None and not isinstance(group, str) and isinstance(group, cabc.Sequence): if ax is not None: raise ValueError("Can only specify `ax` if no `group` sequence is given.") fig, gs = _panel_grid(hspace, wspace, ncols, len(group)) @@ -1374,9 +1352,7 @@ def _get_values_to_plot( column = values_to_plot.replace('log10_', '') else: column = values_to_plot - values_df = pd.pivot( - values_df, index='names', columns='group', values=column - ).fillna(1) + values_df = pd.pivot(values_df, index='names', columns='group', values=column).fillna(1) if values_to_plot in ['log10_pvals', 'log10_pvals_adj']: values_df = -1 * np.log10(values_df) diff --git a/scanpy/plotting/_tools/paga.py b/scanpy/plotting/_tools/paga.py index f5a0cfffe8..7aa9352932 100644 --- a/scanpy/plotting/_tools/paga.py +++ b/scanpy/plotting/_tools/paga.py @@ -147,7 +147,7 @@ def paga_compare( if suptitle is not None: pl.suptitle(suptitle) _utils.savefig_or_show('paga_compare', show=show, save=save) - if show == False: + if not show: return axs @@ -168,7 +168,7 @@ def _compute_pos( if layout == 'fa': try: from fa2 import ForceAtlas2 - except: + except ImportError: logg.warning( "Package 'fa2' is not installed, falling back to layout 'fr'." 'To use the faster and better ForceAtlas2 layout, ' @@ -205,26 +205,19 @@ def _compute_pos( iterations = layout_kwds['iterations'] else: iterations = 500 - pos_list = forceatlas2.forceatlas2( - adjacency_solid, pos=init_coords, iterations=iterations - ) + pos_list = forceatlas2.forceatlas2(adjacency_solid, pos=init_coords, iterations=iterations) pos = {n: [p[0], -p[1]] for n, p in enumerate(pos_list)} elif layout == 'eq_tree': nx_g_tree = nx.Graph(adj_tree) pos = _utils.hierarchy_pos(nx_g_tree, root) if len(pos) < adjacency_solid.shape[0]: - raise ValueError( - 'This is a forest and not a single tree. ' - 'Try another `layout`, e.g., {\'fr\'}.' - ) + raise ValueError('This is a forest and not a single tree. ' 'Try another `layout`, e.g., {\'fr\'}.') else: # igraph layouts g = _sc_utils.get_igraph_from_adjacency(adjacency_solid) if 'rt' in layout: g_tree = _sc_utils.get_igraph_from_adjacency(adj_tree) - pos_list = g_tree.layout( - layout, root=root if isinstance(root, list) else [root] - ).coords + pos_list = g_tree.layout(layout, root=root if isinstance(root, list) else [root]).coords elif layout == 'circle': pos_list = g.layout(layout).coords else: @@ -239,9 +232,7 @@ def _compute_pos( init_pos[:, 1] *= -1 init_coords = init_pos.tolist() try: - pos_list = g.layout( - layout, seed=init_coords, weights='weight', **layout_kwds - ).coords + pos_list = g.layout(layout, seed=init_coords, weights='weight', **layout_kwds).coords except AttributeError: # hack for empty graphs... pos_list = g.layout(layout, seed=init_coords, **layout_kwds).coords pos = {n: [p[0], -p[1]] for n, p in enumerate(pos_list)} @@ -440,18 +431,12 @@ def paga( groups_key = adata.uns['paga']['groups'] def is_flat(x): - has_one_per_category = isinstance(x, cabc.Collection) and len(x) == len( - adata.obs[groups_key].cat.categories - ) + has_one_per_category = isinstance(x, cabc.Collection) and len(x) == len(adata.obs[groups_key].cat.categories) return has_one_per_category or x is None or isinstance(x, str) - if isinstance(colors, cabc.Mapping) and isinstance( - colors[next(iter(colors))], cabc.Mapping - ): + if isinstance(colors, cabc.Mapping) and isinstance(colors[next(iter(colors))], cabc.Mapping): # handle paga pie, remap string keys to integers - names_to_ixs = { - n: i for i, n in enumerate(adata.obs[groups_key].cat.categories) - } + names_to_ixs = {n: i for i, n in enumerate(adata.obs[groups_key].cat.categories)} colors = {names_to_ixs.get(n, n): v for n, v in colors.items()} if is_flat(colors): colors = [colors] @@ -472,21 +457,14 @@ def is_flat(x): if colorbar is None: var_names = adata.var_names if adata.raw is None else adata.raw.var_names colorbars = [ - ( - (c in adata.obs_keys() and adata.obs[c].dtype.name != 'category') - or (c in var_names) - ) - for c in colors + ((c in adata.obs_keys() and adata.obs[c].dtype.name != 'category') or (c in var_names)) for c in colors ] else: colorbars = [False for _ in colors] if isinstance(root, str): if root not in labels: - raise ValueError( - 'If `root` is a string, ' - f'it needs to be one of {labels} not {root!r}.' - ) + raise ValueError('If `root` is a string, ' f'it needs to be one of {labels} not {root!r}.') root = list(labels).index(root) if isinstance(root, cabc.Sequence) and root[0] in labels: root = [list(labels).index(r) for r in root] @@ -578,7 +556,7 @@ def is_flat(x): else: ax_cb = cax[icolor] - cb = pl.colorbar( + _ = pl.colorbar( sct, format=ticker.FuncFormatter(_utils.ticks_formatter), cax=ax_cb, @@ -630,11 +608,7 @@ def _paga_graph( import networkx as nx node_labels = labels # rename for clarity - if ( - node_labels is not None - and isinstance(node_labels, str) - and node_labels != adata.uns['paga']['groups'] - ): + if node_labels is not None and isinstance(node_labels, str) and node_labels != adata.uns['paga']['groups']: raise ValueError( 'Provide a list of group labels for the PAGA groups {}, not {}.'.format( adata.uns['paga']['groups'], node_labels @@ -645,9 +619,9 @@ def _paga_graph( node_labels = adata.obs[groups_key].cat.categories if (colors is None or colors == groups_key) and groups_key is not None: - if groups_key + '_colors' not in adata.uns or len( - adata.obs[groups_key].cat.categories - ) != len(adata.uns[groups_key + '_colors']): + if groups_key + '_colors' not in adata.uns or len(adata.obs[groups_key].cat.categories) != len( + adata.uns[groups_key + '_colors'] + ): _utils.add_colors_for_categorical_sample_annotation(adata, groups_key) colors = adata.uns[groups_key + '_colors'] for iname, name in enumerate(adata.obs[groups_key].cat.categories): @@ -713,11 +687,7 @@ def _paga_graph( colors = x_color # plot continuous annotation - if ( - isinstance(colors, str) - and colors in adata.obs - and not is_categorical_dtype(adata.obs[colors]) - ): + if isinstance(colors, str) and colors in adata.obs and not is_categorical_dtype(adata.obs[colors]): x_color = [] cats = adata.obs[groups_key].cat.categories for icat, cat in enumerate(cats): @@ -726,11 +696,7 @@ def _paga_graph( colors = x_color # plot categorical annotation - if ( - isinstance(colors, str) - and colors in adata.obs - and is_categorical_dtype(adata.obs[colors]) - ): + if isinstance(colors, str) and colors in adata.obs and is_categorical_dtype(adata.obs[colors]): asso_names, asso_matrix = _sc_utils.compute_association_matrix_of_groups( adata, prediction=groups_key, @@ -738,16 +704,11 @@ def _paga_graph( normalization='reference' if normalize_to_color else 'prediction', ) _utils.add_colors_for_categorical_sample_annotation(adata, colors) - asso_colors = _sc_utils.get_associated_colors_of_groups( - adata.uns[colors + '_colors'], asso_matrix - ) + asso_colors = _sc_utils.get_associated_colors_of_groups(adata.uns[colors + '_colors'], asso_matrix) colors = asso_colors if len(colors) != len(node_labels): - raise ValueError( - f'Expected `colors` to be of length `{len(node_labels)}`, ' - f'found `{len(colors)}`.' - ) + raise ValueError(f'Expected `colors` to be of length `{len(node_labels)}`, ' f'found `{len(colors)}`.') # count number of connected components n_components, labels = scipy.sparse.csgraph.connected_components(adjacency_solid) @@ -763,13 +724,8 @@ def _paga_graph( adjacency_solid = adjacency_solid.tocsc()[:, labels == largest_component] colors = np.array(colors)[labels == largest_component] node_labels = np.array(node_labels)[labels == largest_component] - cats_dropped = ( - adata.obs[groups_key].cat.categories[labels != largest_component].tolist() - ) - logg.info( - 'Restricting graph to largest connected component by dropping categories\n' - f'{cats_dropped}' - ) + cats_dropped = adata.obs[groups_key].cat.categories[labels != largest_component].tolist() + logg.info('Restricting graph to largest connected component by dropping categories\n' f'{cats_dropped}') nx_g_solid = nx.Graph(adjacency_solid) if dashed_edges is not None: raise ValueError('`single_component` only if `dashed_edges` is `None`.') @@ -801,9 +757,7 @@ def _paga_graph( widths = np.clip(widths, min_edge_width, max_edge_width) with warnings.catch_warnings(): warnings.simplefilter("ignore") - nx.draw_networkx_edges( - nx_g_solid, pos, ax=ax, width=widths, edge_color='black' - ) + nx.draw_networkx_edges(nx_g_solid, pos, ax=ax, width=widths, edge_color='black') # draw directed edges else: adjacency_transitions = adata.uns['paga'][transitions].copy() @@ -816,9 +770,7 @@ def _paga_graph( widths = base_edge_width * np.array(widths) if min_edge_width is not None or max_edge_width is not None: widths = np.clip(widths, min_edge_width, max_edge_width) - nx.draw_networkx_edges( - g_dir, pos, ax=ax, width=widths, edge_color='black', arrowsize=arrowsize - ) + nx.draw_networkx_edges(g_dir, pos, ax=ax, width=widths, edge_color='black', arrowsize=arrowsize) if export_to_gexf: if isinstance(colors[0], tuple): @@ -850,21 +802,15 @@ def _paga_graph( else: groups_sizes = np.ones(len(node_labels)) base_scale_scatter = 2000 - base_pie_size = ( - base_scale_scatter / (np.sqrt(adjacency_solid.shape[0]) + 10) * node_size_scale - ) + base_pie_size = base_scale_scatter / (np.sqrt(adjacency_solid.shape[0]) + 10) * node_size_scale median_group_size = np.median(groups_sizes) - groups_sizes = base_pie_size * np.power( - groups_sizes / median_group_size, node_size_power - ) + groups_sizes = base_pie_size * np.power(groups_sizes / median_group_size, node_size_power) if fontsize is None: fontsize = rcParams['legend.fontsize'] if fontoutline is not None: text_kwds = dict(text_kwds) - text_kwds['path_effects'] = [ - patheffects.withStroke(linewidth=fontoutline, foreground='w') - ] + text_kwds['path_effects'] = [patheffects.withStroke(linewidth=fontoutline, foreground='w')] # usual scatter plot if not isinstance(colors[0], cabc.Mapping): n_groups = len(pos_array) @@ -892,8 +838,7 @@ def _paga_graph( for ix, (xx, yy) in enumerate(zip(pos_array[:, 0], pos_array[:, 1])): if not isinstance(colors[ix], cabc.Mapping): raise ValueError( - f'{colors[ix]} is neither a dict of valid ' - 'matplotlib colors nor a valid matplotlib color.' + f'{colors[ix]} is neither a dict of valid ' 'matplotlib colors nor a valid matplotlib color.' ) color_single = colors[ix].keys() fracs = [colors[ix][c] for c in color_single] @@ -904,10 +849,7 @@ def _paga_graph( color_single.append('grey') fracs.append(1 - sum(fracs)) elif not np.isclose(total, 1): - raise ValueError( - f'Expected fractions for node `{ix}` to be ' - f'close to 1, found `{total}`.' - ) + raise ValueError(f'Expected fractions for node `{ix}` to be ' f'close to 1, found `{total}`.') cumsum = np.cumsum(fracs) cumsum = cumsum / cumsum[-1] @@ -921,9 +863,7 @@ def _paga_graph( xy = np.column_stack([x, y]) s = np.abs(xy).max() - sct = ax.scatter( - [xx], [yy], marker=xy, s=s ** 2 * groups_sizes[ix], color=color - ) + sct = ax.scatter([xx], [yy], marker=xy, s=s ** 2 * groups_sizes[ix], color=color) if node_labels is not None: ax.text( @@ -947,9 +887,7 @@ def paga_path( use_raw: bool = True, annotations: Sequence[str] = ('dpt_pseudotime',), color_map: Union[str, Colormap, None] = None, - color_maps_annotations: Mapping[str, Union[str, Colormap]] = MappingProxyType( - dict(dpt_pseudotime='Greys') - ), + color_maps_annotations: Mapping[str, Union[str, Colormap]] = MappingProxyType(dict(dpt_pseudotime='Greys')), palette_groups: Optional[Sequence[str]] = None, n_avg: int = 1, groups_key: Optional[str] = None, @@ -1032,17 +970,13 @@ def paga_path( if groups_key is None: if 'groups' not in adata.uns['paga']: - raise KeyError( - 'Pass the key of the grouping with which you ran PAGA, ' - 'using the parameter `groups_key`.' - ) + raise KeyError('Pass the key of the grouping with which you ran PAGA, ' 'using the parameter `groups_key`.') groups_key = adata.uns['paga']['groups'] groups_names = adata.obs[groups_key].cat.categories if 'dpt_pseudotime' not in adata.obs.keys(): raise ValueError( - '`pl.paga_path` requires computation of a pseudotime `tl.dpt` ' - 'for ordering at single-cell resolution' + '`pl.paga_path` requires computation of a pseudotime `tl.dpt` ' 'for ordering at single-cell resolution' ) if palette_groups is None: @@ -1081,9 +1015,7 @@ def moving_average(a): for ikey, key in enumerate(keys): x = [] for igroup, group in enumerate(nodes_ints): - idcs = np.arange(adata.n_obs)[ - adata.obs[groups_key].values == nodes_strs[igroup] - ] + idcs = np.arange(adata.n_obs)[adata.obs[groups_key].values == nodes_strs[igroup]] if len(idcs) == 0: raise ValueError( 'Did not find data points that match ' @@ -1092,9 +1024,7 @@ def moving_average(a): 'actually contains what you expect.' ) idcs_group = np.argsort( - adata.obs['dpt_pseudotime'].values[ - adata.obs[groups_key].values == nodes_strs[igroup] - ] + adata.obs['dpt_pseudotime'].values[adata.obs[groups_key].values == nodes_strs[igroup]] ) idcs = idcs[idcs_group] if key in adata.obs_keys(): @@ -1220,9 +1150,7 @@ def moving_average(a): ) arr = np.array(anno_dict[anno])[None, :] if anno not in color_maps_annotations: - color_map_anno = ( - 'Vega10' if is_categorical_dtype(adata.obs[anno]) else 'Greys' - ) + color_map_anno = 'Vega10' if is_categorical_dtype(adata.obs[anno]) else 'Greys' else: color_map_anno = color_maps_annotations[anno] img = anno_axis.imshow( diff --git a/scanpy/plotting/_tools/scatterplots.py b/scanpy/plotting/_tools/scatterplots.py index 68c4c8b836..9722caa329 100644 --- a/scanpy/plotting/_tools/scatterplots.py +++ b/scanpy/plotting/_tools/scatterplots.py @@ -143,8 +143,7 @@ def embedding( use_raw = layer is None and adata.raw is not None if use_raw and layer is not None: raise ValueError( - "Cannot use both a layer and the raw representation. Was passed:" - f"use_raw={use_raw}, layer={layer}." + "Cannot use both a layer and the raw representation. Was passed:" f"use_raw={use_raw}, layer={layer}." ) if wspace is None: @@ -152,10 +151,7 @@ def embedding( # current figure size wspace = 0.75 / rcParams['figure.figsize'][0] + 0.02 if adata.raw is None and use_raw: - raise ValueError( - "`use_raw` is set to True but AnnData object does not have raw. " - "Please check." - ) + raise ValueError("`use_raw` is set to True but AnnData object does not have raw. " "Please check.") # turn color into a python list color = [color] if isinstance(color, str) or color is None else list(color) if title is not None: @@ -164,24 +160,17 @@ def embedding( # get the points position and the components list # (only if components is not None) - data_points, components_list = _get_data_points( - adata, basis, projection, components, scale_factor - ) + data_points, components_list = _get_data_points(adata, basis, projection, components, scale_factor) # Setup layout. # Most of the code is for the case when multiple plots are required # 'color' is a list of names that want to be plotted. # Eg. ['Gene1', 'louvain', 'Gene2']. # component_list is a list of components [[0,1], [1,2]] - if ( - not isinstance(color, str) - and isinstance(color, cabc.Sequence) - and len(color) > 1 - ) or len(components_list) > 1: + if (not isinstance(color, str) and isinstance(color, cabc.Sequence) and len(color) > 1) or len(components_list) > 1: if ax is not None: raise ValueError( - "Cannot specify `ax` when plotting multiple panels " - "(each for a given value of 'color')." + "Cannot specify `ax` when plotting multiple panels " "(each for a given value of 'color')." ) if len(components_list) == 0: components_list = [None] @@ -233,9 +222,7 @@ def embedding( # color=gene1, components=[1,2], color=gene1, components=[2,3], # color=gene2, components = [1, 2], color=gene2, components=[2,3], # ] - for count, (value_to_plot, component_idx) in enumerate( - itertools.product(color, idx_components) - ): + for count, (value_to_plot, component_idx) in enumerate(itertools.product(color, idx_components)): color_source_vector = _get_color_source_vector( adata, value_to_plot, @@ -252,7 +239,7 @@ def embedding( na_color=na_color, ) - ### Order points + # Order points order = slice(None) if sort_order is True and value_to_plot is not None and categorical is False: # Higher values plotted on top, null values on bottom @@ -293,9 +280,7 @@ def embedding( if categorical: kwargs['vmin'] = kwargs['vmax'] = None else: - kwargs['vmin'], kwargs['vmax'] = _get_vmin_vmax( - vmin, vmax, count, color_vector - ) + kwargs['vmin'], kwargs['vmax'] = _get_vmin_vmax(vmin, vmax, count, color_vector) # make the scatter plot if projection == '3d': @@ -404,9 +389,7 @@ def embedding( continue if legend_fontoutline is not None: - path_effect = [ - patheffects.withStroke(linewidth=legend_fontoutline, foreground='w') - ] + path_effect = [patheffects.withStroke(linewidth=legend_fontoutline, foreground='w')] else: path_effect = None @@ -470,7 +453,6 @@ def _get_vmin_vmax( index: int, color_vector: Sequence[float], ) -> Tuple[Union[float, None], Union[float, None]]: - """ Evaluates the value of vmin and vmax, which could be a str in which case is interpreted as a percentile and should @@ -561,17 +543,11 @@ def _wraps_plot_scatter(wrapper): wrapper_params.pop("adata") params.update(wrapper_params) - annotations = { - k: v.annotation - for k, v in params.items() - if v.annotation != inspect.Parameter.empty - } + annotations = {k: v.annotation for k, v in params.items() if v.annotation != inspect.Parameter.empty} if wrapper_sig.return_annotation is not inspect.Signature.empty: annotations["return"] = wrapper_sig.return_annotation - wrapper.__signature__ = inspect.Signature( - list(params.values()), return_annotation=wrapper_sig.return_annotation - ) + wrapper.__signature__ = inspect.Signature(list(params.values()), return_annotation=wrapper_sig.return_annotation) wrapper.__annotations__ = annotations return wrapper @@ -660,9 +636,7 @@ def diffmap(adata, **kwargs) -> Union[Axes, List[Axes], None]: scatter_bulk=doc_scatter_embedding, show_save_ax=doc_show_save_ax, ) -def draw_graph( - adata: AnnData, *, layout: Optional[_IGraphLayout] = None, **kwargs -) -> Union[Axes, List[Axes], None]: +def draw_graph(adata: AnnData, *, layout: Optional[_IGraphLayout] = None, **kwargs) -> Union[Axes, List[Axes], None]: """\ Scatter plot in graph-drawing basis. @@ -685,9 +659,7 @@ def draw_graph( basis = 'draw_graph_' + layout if 'X_' + basis not in adata.obsm_keys(): raise ValueError( - 'Did not find {} in adata.obs. Did you compute layout {}?'.format( - 'draw_graph_' + layout, layout - ) + 'Did not find {} in adata.obs. Did you compute layout {}?'.format('draw_graph_' + layout, layout) ) return embedding(adata, basis, **kwargs) @@ -725,15 +697,12 @@ def pca( If `show==False` a :class:`~matplotlib.axes.Axes` or a list of it. """ if not annotate_var_explained: - return embedding( - adata, 'pca', show=show, return_fig=return_fig, save=save, **kwargs - ) + return embedding(adata, 'pca', show=show, return_fig=return_fig, save=save, **kwargs) else: - if 'pca' not in adata.obsm.keys() and f"X_pca" not in adata.obsm.keys(): + if 'pca' not in adata.obsm.keys() and 'X_pca' not in adata.obsm.keys(): raise KeyError( - f"Could not find entry in `obsm` for 'pca'.\n" - f"Available keys are: {list(adata.obsm.keys())}." + f"Could not find entry in `obsm` for 'pca'.\n" f"Available keys are: {list(adata.obsm.keys())}." ) label_dict = { @@ -845,9 +814,7 @@ def spatial( library_id, spatial_data = _check_spatial_data(adata.uns, library_id) img, img_key = _check_img(spatial_data, img, img_key, bw=bw) spot_size = _check_spot_size(spatial_data, spot_size) - scale_factor = _check_scale_factor( - spatial_data, img_key=img_key, scale_factor=scale_factor - ) + scale_factor = _check_scale_factor(spatial_data, img_key=img_key, scale_factor=scale_factor) crop_coord = _check_crop_coord(crop_coord, scale_factor) na_color = _check_na_color(na_color, img=img) @@ -914,8 +881,7 @@ def _get_data_points( basis_key = f"X_{basis}" else: raise KeyError( - f"Could not find entry in `obsm` for '{basis}'.\n" - f"Available keys are: {list(adata.obsm.keys())}." + f"Could not find entry in `obsm` for '{basis}'.\n" f"Available keys are: {list(adata.obsm.keys())}." ) n_dims = 2 @@ -935,9 +901,7 @@ def _get_data_points( r_value = 3 if projection == '3d' else 2 _components_list = np.arange(adata.obsm[basis_key].shape[1]) + 1 - components = [ - ",".join(map(str, x)) for x in combinations(_components_list, r=r_value) - ] + components = [",".join(map(str, x)) for x in combinations(_components_list, r=r_value)] components_list = [] offset = 0 @@ -949,9 +913,7 @@ def _get_data_points( if isinstance(components, str): # eg: components='1,2' - components_list.append( - tuple(int(x.strip()) - 1 + offset for x in components.split(',')) - ) + components_list.append(tuple(int(x.strip()) - 1 + offset for x in components.split(','))) elif isinstance(components, cabc.Sequence): if isinstance(components[0], int): @@ -963,32 +925,26 @@ def _get_data_points( # More than one component can be given and is stored # as a new item of components_list for comp in components: - components_list.append( - tuple(int(x.strip()) - 1 + offset for x in comp.split(',')) - ) + components_list.append(tuple(int(x.strip()) - 1 + offset for x in comp.split(','))) else: raise ValueError( - "Given components: '{}' are not valid. Please check. " - "A valid example is `components='2,3'`" + "Given components: '{}' are not valid. Please check. " "A valid example is `components='2,3'`" ) # check if the components are present in the data try: data_points = [] for comp in components_list: data_points.append(adata.obsm[basis_key][:, comp]) - except: + except Exception: raise ValueError( - "Given components: '{}' are not valid. Please check. " - "A valid example is `components='2,3'`" + "Given components: '{}' are not valid. Please check. " "A valid example is `components='2,3'`" ) if basis == 'diffmap': # remove the offset added in the case of diffmap, such that # plot_scatter can print the labels correctly. - components_list = [ - tuple(number - 1 for number in comp) for comp in components_list - ] + components_list = [tuple(number - 1 for number in comp) for comp in components_list] else: data_points = [np.array(adata.obsm[basis_key])[:, offset : offset + n_dims]] components_list = [] @@ -1015,9 +971,7 @@ def _add_categorical_legend( """Add a legend to the passed Axes.""" if na_in_legend and pd.isnull(color_source_vector).any(): if "NA" in color_source_vector: - raise NotImplementedError( - "No fallback for null labels has been defined if NA already in categories." - ) + raise NotImplementedError("No fallback for null labels has been defined if NA already in categories.") color_source_vector = color_source_vector.add_categories("NA").fillna("NA") palette = palette.copy() palette["NA"] = na_color @@ -1041,11 +995,7 @@ def _add_categorical_legend( ) elif legend_loc == 'on data': # identify centroids to put labels - all_pos = ( - pd.DataFrame(scatter_array, columns=["x", "y"]) - .groupby(color_source_vector, observed=True) - .median() - ) + all_pos = pd.DataFrame(scatter_array, columns=["x", "y"]).groupby(color_source_vector, observed=True).median() for label, x_pos, y_pos in all_pos.itertuples(): ax.text( @@ -1063,9 +1013,7 @@ def _add_categorical_legend( _utils._tmp_cluster_pos = all_pos.values -def _get_color_source_vector( - adata, value_to_plot, use_raw=False, gene_symbols=None, layer=None, groups=None -): +def _get_color_source_vector(adata, value_to_plot, use_raw=False, gene_symbols=None, layer=None, groups=None): """ Get array from adata that colors will be based on. """ @@ -1075,11 +1023,7 @@ def _get_color_source_vector( # _color_vector handles this. # https://github.com/matplotlib/matplotlib/issues/18294 return np.broadcast_to(np.nan, adata.n_obs) - if ( - gene_symbols is not None - and value_to_plot not in adata.obs.columns - and value_to_plot not in adata.var_names - ): + if gene_symbols is not None and value_to_plot not in adata.obs.columns and value_to_plot not in adata.var_names: # We should probably just make an index for this, and share it over runs value_to_plot = adata.var.index[adata.var[gene_symbols] == value_to_plot][ 0 @@ -1098,9 +1042,7 @@ def _get_palette(adata, values_key: str, palette=None): values = pd.Categorical(adata.obs[values_key]) if palette: _utils._set_colors_for_categorical_obs(adata, values_key, palette) - elif color_key not in adata.uns or len(adata.uns[color_key]) < len( - values.categories - ): + elif color_key not in adata.uns or len(adata.uns[color_key]) < len(values.categories): # set a default palette in case that no colors or few colors are found _utils._set_default_colors_for_categorical_obs(adata, values_key) else: @@ -1108,9 +1050,7 @@ def _get_palette(adata, values_key: str, palette=None): return dict(zip(values.categories, adata.uns[color_key])) -def _color_vector( - adata, values_key: str, values, palette, na_color="lightgray" -) -> Tuple[np.ndarray, bool]: +def _color_vector(adata, values_key: str, values, palette, na_color="lightgray") -> Tuple[np.ndarray, bool]: """ Map array of values to array of hex (plus alpha) codes. @@ -1129,10 +1069,7 @@ def _color_vector( if not is_categorical_dtype(values): return values, False else: # is_categorical_dtype(values) - color_map = { - k: to_hex(v) - for k, v in _get_palette(adata, values_key, palette=palette).items() - } + color_map = {k: to_hex(v) for k, v in _get_palette(adata, values_key, palette=palette).items()} # If color_map does not have unique values, this can be slow as the # result is not categorical color_vector = values.map(color_map) @@ -1165,19 +1102,14 @@ def _basis2name(basis): return component_name -def _check_spot_size( - spatial_data: Optional[Mapping], spot_size: Optional[float] -) -> float: +def _check_spot_size(spatial_data: Optional[Mapping], spot_size: Optional[float]) -> float: """ Resolve spot_size value. This is a required argument for spatial plots. """ if spatial_data is None and spot_size is None: - raise ValueError( - "When .uns['spatial'][library_id] does not exist, spot_size must be " - "provided directly." - ) + raise ValueError("When .uns['spatial'][library_id] does not exist, spot_size must be " "provided directly.") elif spot_size is None: return spatial_data['scalefactors']['spot_diameter_fullres'] else: @@ -1198,9 +1130,7 @@ def _check_scale_factor( return 1.0 -def _check_spatial_data( - uns: Mapping, library_id: Union[Empty, None, str] -) -> Tuple[Optional[str], Optional[Mapping]]: +def _check_spatial_data(uns: Mapping, library_id: Union[Empty, None, str]) -> Tuple[Optional[str], Optional[Mapping]]: """ Given a mapping, try and extract a library id/ mapping with spatial data. @@ -1257,9 +1187,7 @@ def _check_crop_coord( return crop_coord -def _check_na_color( - na_color: Optional[ColorLike], *, img: Optional[np.ndarray] = None -) -> ColorLike: +def _check_na_color(na_color: Optional[ColorLike], *, img: Optional[np.ndarray] = None) -> ColorLike: if na_color is None: if img is not None: na_color = (0.0, 0.0, 0.0, 0.0) diff --git a/scanpy/plotting/_utils.py b/scanpy/plotting/_utils.py index f6cb8886ba..214019755f 100644 --- a/scanpy/plotting/_utils.py +++ b/scanpy/plotting/_utils.py @@ -28,9 +28,7 @@ ColorLike = Union[str, Tuple[float, ...]] _IGraphLayout = Literal['fa', 'fr', 'rt', 'rt_circular', 'drl', 'eq_tree', ...] _FontWeight = Literal['light', 'normal', 'medium', 'semibold', 'bold', 'heavy', 'black'] -_FontSize = Literal[ - 'xx-small', 'x-small', 'small', 'medium', 'large', 'x-large', 'xx-large' -] +_FontSize = Literal['xx-small', 'x-small', 'small', 'medium', 'large', 'x-large', 'xx-large'] class _AxesSubplot(Axes, axes.SubplotBase, ABC): @@ -69,9 +67,7 @@ def matrix( ax.set_xticks(range(len(xticks)), xticks, rotation='vertical') if yticks is not None: ax.set_yticks(range(len(yticks)), yticks) - pl.colorbar( - img, shrink=colorbar_shrink, ax=ax - ) # need a figure instance for colorbar + pl.colorbar(img, shrink=colorbar_shrink, ax=ax) # need a figure instance for colorbar savefig_or_show('matrix', show=show, save=save) @@ -126,7 +122,7 @@ def timeseries_subplot( else: levels, _ = np.unique(color, return_inverse=True) colors = np.array(palette[: len(levels)].by_key()['color']) - subsets = [(x_range[color == l], X[color == l, :]) for l in levels] + subsets = [(x_range[color == level], X[color == level, :]) for level in levels] if ax is None: ax = pl.subplot() @@ -156,9 +152,7 @@ def timeseries_subplot( ax.legend(frameon=False) -def timeseries_as_heatmap( - X: np.ndarray, var_names: Collection[str] = (), highlights_x=(), color_map=None -): +def timeseries_as_heatmap(X: np.ndarray, var_names: Collection[str] = (), highlights_x=(), color_map=None): """\ Plot timeseries as heatmap. @@ -267,10 +261,7 @@ def savefig(writekey, dpi=None, ext=None): """ if dpi is None: # we need this as in notebooks, the internal figures are also influenced by 'savefig.dpi' this... - if ( - not isinstance(rcParams['savefig.dpi'], str) - and rcParams['savefig.dpi'] < 150 - ): + if not isinstance(rcParams['savefig.dpi'], str) and rcParams['savefig.dpi'] < 150: if settings._low_resolution_warning: logg.warning( 'You are using a low resolution (dpi<150) for saving figures.\n' @@ -358,9 +349,7 @@ def _validate_palette(adata, key): adata.uns[color_key] = _palette -def _set_colors_for_categorical_obs( - adata, value_to_plot, palette: Union[str, Sequence[str], Cycler] -): +def _set_colors_for_categorical_obs(adata, value_to_plot, palette: Union[str, Sequence[str], Cycler]): """ Sets the adata.uns[value_to_plot + '_colors'] according to the given palette @@ -409,10 +398,7 @@ def _set_colors_for_categorical_obs( if color in additional_colors: color = additional_colors[color] else: - raise ValueError( - "The following color value of the given palette " - f"is not valid: {color}" - ) + raise ValueError("The following color value of the given palette " f"is not valid: {color}") _color_list.append(color) palette = cycler(color=_color_list) @@ -472,9 +458,7 @@ def _set_default_colors_for_categorical_obs(adata, value_to_plot): adata.uns[value_to_plot + '_colors'] = palette[:length] -def add_colors_for_categorical_sample_annotation( - adata, key, palette=None, force_update_colors=False -): +def add_colors_for_categorical_sample_annotation(adata, key, palette=None, force_update_colors=False): color_key = f"{key}_colors" colors_needed = len(adata.obs[key].cat.categories) @@ -517,14 +501,10 @@ def plot_edges(axs, adata, basis, edges_width, edges_color, neighbors_key=None): def plot_arrows(axs, adata, basis, arrows_kwds=None): if not isinstance(axs, cabc.Sequence): axs = [axs] - v_prefix = next( - (p for p in ['velocity', 'Delta'] if f'{p}_{basis}' in adata.obsm), None - ) + v_prefix = next((p for p in ['velocity', 'Delta'] if f'{p}_{basis}' in adata.obsm), None) if v_prefix is None: raise ValueError( - "`arrows=True` requires " - f"`'velocity_{basis}'` from scvelo or " - f"`'Delta_{basis}'` from velocyto." + "`arrows=True` requires " f"`'velocity_{basis}'` from scvelo or " f"`'Delta_{basis}'` from velocyto." ) if v_prefix == 'velocity': logg.warning( @@ -606,9 +586,7 @@ def setup_axes( if show_ticks: base_width *= 1.1 - draw_region_width = ( - base_width - left_offset - top_offset - 0.5 - ) # this is kept constant throughout + draw_region_width = base_width - left_offset - top_offset - 0.5 # this is kept constant throughout right_margin_factor = sum([1 + right_margin for right_margin in right_margin_list]) width_without_offsets = ( @@ -619,7 +597,7 @@ def setup_axes( figure_width = width_without_offsets + left_offset + right_offset draw_region_width_frac = draw_region_width / figure_width left_offset_frac = left_offset / figure_width - right_offset_frac = 1 - (len(panels) - 1) * left_offset_frac + right_offset_frac = 1 - (len(panels) - 1) * left_offset_frac # noqa: F841 if ax is None: pl.figure( @@ -629,9 +607,7 @@ def setup_axes( left_positions = [left_offset_frac, left_offset_frac + draw_region_width_frac] for i in range(1, len(panels)): right_margin = right_margin_list[i - 1] - left_positions.append( - left_positions[-1] + right_margin * draw_region_width_frac - ) + left_positions.append(left_positions[-1] + right_margin * draw_region_width_frac) left_positions.append(left_positions[-1] + draw_region_width_frac) panel_pos = [[bottom_offset], [1 - top_offset], left_positions] @@ -735,16 +711,11 @@ def scatter_base( ) if colorbars[icolor]: width = 0.006 * draw_region_width / len(colors) - left = ( - panel_pos[2][2 * icolor + 1] - + (1.2 if projection == '3d' else 0.2) * width - ) + left = panel_pos[2][2 * icolor + 1] + (1.2 if projection == '3d' else 0.2) * width rectangle = [left, bottom, width, height] fig = pl.gcf() ax_cb = fig.add_axes(rectangle) - cb = pl.colorbar( - sct, format=ticker.FuncFormatter(ticks_formatter), cax=ax_cb - ) + pl.colorbar(sct, format=ticker.FuncFormatter(ticks_formatter), cax=ax_cb) # set the title if title is not None: ax.set_title(title[icolor]) @@ -763,11 +734,7 @@ def scatter_base( s=10, zorder=20, ) - highlight_text = ( - highlights_labels[iihighlight] - if len(highlights_labels) > 0 - else str(ihighlight) - ) + highlight_text = highlights_labels[iihighlight] if len(highlights_labels) > 0 else str(ihighlight) # the following is a Python 2 compatibility hack ax.text( *([d[0] for d in data] + [highlight_text]), @@ -782,10 +749,7 @@ def scatter_base( ax.set_zticks([]) # set default axis_labels if axis_labels is None: - axis_labels = [ - [component_name + str(i) for i in component_indexnames] - for _ in range(len(axs)) - ] + axis_labels = [[component_name + str(i) for i in component_indexnames] for _ in range(len(axs))] else: axis_labels = [axis_labels for _ in range(len(axs))] for iax, ax in enumerate(axs): @@ -939,8 +903,8 @@ def make_pos(pos, node=root, currentLevel=0, parent=None, vert_loc=0): if levels is None: levels = make_levels({}) else: - levels = {l: {TOTAL: levels[l], CURRENT: 0} for l in levels} - vert_gap = height / (max([l for l in levels]) + 1) + levels = {level: {TOTAL: levels[level], CURRENT: 0} for level in levels} + vert_gap = height / (max([level for level in levels]) + 1) return make_pos({}) @@ -973,9 +937,7 @@ def zoom(ax, xy='x', factor=1): ---------- """ limits = ax.get_xlim() if xy == 'x' else ax.get_ylim() - new_limits = 0.5 * (limits[0] + limits[1]) + 1.0 / factor * np.array( - (-0.5, 0.5) - ) * (limits[1] - limits[0]) + new_limits = 0.5 * (limits[0] + limits[1]) + 1.0 / factor * np.array((-0.5, 0.5)) * (limits[1] - limits[0]) if xy == 'x': ax.set_xlim(new_limits) else: @@ -1057,9 +1019,7 @@ def check_projection(projection): mpl_version = parse(mpl.__version__) if mpl_version < parse("3.3.3"): - raise ImportError( - f"3d plotting requires matplotlib > 3.3.3. Found {mpl.__version__}" - ) + raise ImportError(f"3d plotting requires matplotlib > 3.3.3. Found {mpl.__version__}") def circles(x, y, s, ax, marker=None, c='b', vmin=None, vmax=None, **kwargs): diff --git a/scanpy/plotting/palettes.py b/scanpy/plotting/palettes.py index 7086549a4c..f173719049 100644 --- a/scanpy/plotting/palettes.py +++ b/scanpy/plotting/palettes.py @@ -1,5 +1,6 @@ """Color palettes in addition to matplotlib's palettes.""" +from typing import Mapping, Sequence from matplotlib import cm, colors # Colorblindness adjusted vega_10 @@ -180,9 +181,6 @@ default_102 = godsnot_102 -from typing import Mapping, Sequence - - def _plot_color_cycle(clists: Mapping[str, Sequence[str]]): import numpy as np import matplotlib.pyplot as plt @@ -212,6 +210,4 @@ def _plot_color_cycle(clists: Mapping[str, Sequence[str]]): if __name__ == '__main__': - _plot_color_cycle( - {name: colors for name, colors in globals().items() if isinstance(colors, list)} - ) + _plot_color_cycle({name: colors for name, colors in globals().items() if isinstance(colors, list)}) diff --git a/scanpy/preprocessing/_combat.py b/scanpy/preprocessing/_combat.py index e2d8140bca..ee5761798c 100644 --- a/scanpy/preprocessing/_combat.py +++ b/scanpy/preprocessing/_combat.py @@ -10,9 +10,7 @@ from .._utils import sanitize_anndata -def _design_matrix( - model: pd.DataFrame, batch_key: str, batch_levels: Collection[str] -) -> pd.DataFrame: +def _design_matrix(model: pd.DataFrame, batch_key: str, batch_levels: Collection[str]) -> pd.DataFrame: """\ Computes a simple design matrix. @@ -44,9 +42,7 @@ def _design_matrix( if other_cols: col_repr = " + ".join("Q('{}')".format(x) for x in other_cols) - factor_matrix = patsy.dmatrix( - "~ 0 + {}".format(col_repr), model[other_cols], return_type="dataframe" - ) + factor_matrix = patsy.dmatrix("~ 0 + {}".format(col_repr), model[other_cols], return_type="dataframe") design = pd.concat((design, factor_matrix), axis=1) logg.info(f"Found {len(other_cols)} categorical variables:") @@ -109,9 +105,7 @@ def _standardize_data( # Compute the means if np.sum(var_pooled == 0) > 0: print(f'Found {np.sum(var_pooled == 0)} genes with zero variance.') - stand_mean = np.dot( - grand_mean.T.reshape((len(grand_mean), 1)), np.ones((1, int(n_array))) - ) + stand_mean = np.dot(grand_mean.T.reshape((len(grand_mean), 1)), np.ones((1, int(n_array)))) tmp = np.array(design.copy()) tmp[:, :n_batch] = 0 stand_mean += np.dot(tmp, B_hat).T @@ -175,9 +169,7 @@ def combat( cov_exist = np.isin(covariates, adata.obs_keys()) if np.any(~cov_exist): missing_cov = np.array(covariates)[~cov_exist].tolist() - raise ValueError( - 'Could not find the covariate(s) {!r} in adata.obs'.format(missing_cov) - ) + raise ValueError('Could not find the covariate(s) {!r} in adata.obs'.format(missing_cov)) if key in covariates: raise ValueError('Batch key and covariates cannot overlap') @@ -209,9 +201,7 @@ def combat( logg.info("Fitting L/S model and finding priors\n") batch_design = design[design.columns[:n_batch]] # first estimate of the additive batch effect - gamma_hat = ( - la.inv(batch_design.T @ batch_design) @ batch_design.T @ s_data.T - ).values + gamma_hat = (la.inv(batch_design.T @ batch_design) @ batch_design.T @ s_data.T).values delta_hat = [] # first estimate for the multiplicative batch effect @@ -260,10 +250,7 @@ def combat( dsq = np.sqrt(delta_star[j, :]) dsq = dsq.reshape((len(dsq), 1)) denom = np.dot(dsq, np.ones((1, n_batches[j]))) - numer = np.array( - bayesdata.iloc[:, batch_idxs] - - np.dot(batch_design.iloc[batch_idxs], gamma_star).T - ) + numer = np.array(bayesdata.iloc[:, batch_idxs] - np.dot(batch_design.iloc[batch_idxs], gamma_star).T) bayesdata.iloc[:, batch_idxs] = numer / denom vpsq = np.sqrt(var_pooled).reshape((len(var_pooled), 1)) @@ -329,16 +316,12 @@ def _it_sol( # in the loop, gamma and delta are updated together. they depend on each other. we iterate until convergence. while change > conv: g_new = (t2 * n * g_hat + d_old * g_bar) / (t2 * n + d_old) - sum2 = s_data - g_new.reshape((g_new.shape[0], 1)) @ np.ones( - (1, s_data.shape[1]) - ) + sum2 = s_data - g_new.reshape((g_new.shape[0], 1)) @ np.ones((1, s_data.shape[1])) sum2 = sum2 ** 2 sum2 = sum2.sum(axis=1) d_new = (0.5 * sum2 + b) / (n / 2.0 + a - 1.0) - change = max( - (abs(g_new - g_old) / g_old).max(), (abs(d_new - d_old) / d_old).max() - ) + change = max((abs(g_new - g_old) / g_old).max(), (abs(d_new - d_old) / d_old).max()) g_old = g_new # .copy() d_old = d_new # .copy() count = count + 1 diff --git a/scanpy/preprocessing/_deprecated/__init__.py b/scanpy/preprocessing/_deprecated/__init__.py index 2bb8730540..0e768454c1 100644 --- a/scanpy/preprocessing/_deprecated/__init__.py +++ b/scanpy/preprocessing/_deprecated/__init__.py @@ -36,15 +36,9 @@ def normalize_per_cell_weinreb16_deprecated( gene_subset = np.all(X <= counts_per_cell[:, None] * max_fraction, axis=0) if issparse(X): gene_subset = gene_subset.A1 - tc_include = ( - X[:, gene_subset].sum(1).A1 if issparse(X) else X[:, gene_subset].sum(1) - ) + tc_include = X[:, gene_subset].sum(1).A1 if issparse(X) else X[:, gene_subset].sum(1) - X_norm = ( - X.multiply(csr_matrix(1 / tc_include[:, None])) - if issparse(X) - else X / tc_include[:, None] - ) + X_norm = X.multiply(csr_matrix(1 / tc_include[:, None])) if issparse(X) else X / tc_include[:, None] if mult_with_mean: X_norm *= np.mean(counts_per_cell) diff --git a/scanpy/preprocessing/_deprecated/highly_variable_genes.py b/scanpy/preprocessing/_deprecated/highly_variable_genes.py index 570a5f9f25..86d68c3a77 100644 --- a/scanpy/preprocessing/_deprecated/highly_variable_genes.py +++ b/scanpy/preprocessing/_deprecated/highly_variable_genes.py @@ -104,9 +104,7 @@ def filter_genes_dispersion( If a data matrix `X` is passed, the annotation is returned as `np.recarray` with the same information stored in fields: `gene_subset`, `means`, `dispersions`, `dispersion_norm`. """ - if n_top_genes is not None and not all( - x is None for x in [min_disp, max_disp, min_mean, max_mean] - ): + if n_top_genes is not None and not all(x is None for x in [min_disp, max_disp, min_mean, max_mean]): logg.info('If you pass `n_top_genes`, all cutoffs are ignored.') if min_disp is None: min_disp = 0.5 @@ -170,8 +168,7 @@ def filter_genes_dispersion( disp_mean_bin[one_gene_per_bin] = 0 # actually do the normalization df['dispersion_norm'] = ( - df['dispersion'].values # use values here as index differs - - disp_mean_bin[df['mean_bin'].values].values + df['dispersion'].values - disp_mean_bin[df['mean_bin'].values].values # use values here as index differs ) / disp_std_bin[df['mean_bin'].values].values elif flavor == 'cell_ranger': from statsmodels import robust @@ -187,9 +184,7 @@ def filter_genes_dispersion( warnings.simplefilter('ignore') disp_mad_bin = disp_grouped.apply(robust.mad) df['dispersion_norm'] = ( - np.abs( - df['dispersion'].values - disp_median_bin[df['mean_bin'].values].values - ) + np.abs(df['dispersion'].values - disp_median_bin[df['mean_bin'].values].values) / disp_mad_bin[df['mean_bin'].values].values ) else: @@ -197,15 +192,10 @@ def filter_genes_dispersion( dispersion_norm = df['dispersion_norm'].values.astype('float32') if n_top_genes is not None: dispersion_norm = dispersion_norm[~np.isnan(dispersion_norm)] - dispersion_norm[ - ::-1 - ].sort() # interestingly, np.argpartition is slightly slower + dispersion_norm[::-1].sort() # interestingly, np.argpartition is slightly slower disp_cut_off = dispersion_norm[n_top_genes - 1] gene_subset = df['dispersion_norm'].values >= disp_cut_off - logg.debug( - f'the {n_top_genes} top genes correspond to a ' - f'normalized dispersion cutoff of {disp_cut_off}' - ) + logg.debug(f'the {n_top_genes} top genes correspond to a ' f'normalized dispersion cutoff of {disp_cut_off}') else: max_disp = np.inf if max_disp is None else max_disp dispersion_norm[np.isnan(dispersion_norm)] = 0 # similar to Seurat diff --git a/scanpy/preprocessing/_highly_variable_genes.py b/scanpy/preprocessing/_highly_variable_genes.py index dec4449534..c76c57bd48 100644 --- a/scanpy/preprocessing/_highly_variable_genes.py +++ b/scanpy/preprocessing/_highly_variable_genes.py @@ -52,16 +52,11 @@ def _highly_variable_genes_seurat_v3( try: from skmisc.loess import loess except ImportError: - raise ImportError( - 'Please install skmisc package via `pip install --user scikit-misc' - ) + raise ImportError('Please install skmisc package via `pip install --user scikit-misc') X = adata.layers[layer] if layer is not None else adata.X if check_nonnegative_integers(X) is False: - raise ValueError( - "`pp.highly_variable_genes` with `flavor='seurat_v3'` expects " - "raw count data." - ) + raise ValueError("`pp.highly_variable_genes` with `flavor='seurat_v3'` expects " "raw count data.") if batch_key is None: batch_info = pd.Categorical(np.zeros(adata.shape[0], dtype=int)) @@ -110,9 +105,7 @@ def _highly_variable_genes_seurat_v3( batch_counts_sum = batch_counts.sum(axis=0) norm_gene_var = (1 / ((N - 1) * np.square(reg_std))) * ( - (N * np.square(mean)) - + squared_batch_counts_sum - - 2 * batch_counts_sum * mean + (N * np.square(mean)) + squared_batch_counts_sum - 2 * batch_counts_sum * mean ) norm_gene_vars.append(norm_gene_var.reshape(1, -1)) @@ -122,9 +115,7 @@ def _highly_variable_genes_seurat_v3( # this is done in SelectIntegrationFeatures() in Seurat v3 ranked_norm_gene_vars = ranked_norm_gene_vars.astype(np.float32) - num_batches_high_var = np.sum( - (ranked_norm_gene_vars < n_top_genes).astype(int), axis=0 - ) + num_batches_high_var = np.sum((ranked_norm_gene_vars < n_top_genes).astype(int), axis=0) ranked_norm_gene_vars[ranked_norm_gene_vars >= n_top_genes] = np.nan ma_ranked = np.ma.masked_invalid(ranked_norm_gene_vars) median_ranked = np.ma.median(ma_ranked, axis=0).filled(np.nan) @@ -160,13 +151,9 @@ def _highly_variable_genes_seurat_v3( adata.var['highly_variable_rank'] = df['highly_variable_rank'].values adata.var['means'] = df['means'].values adata.var['variances'] = df['variances'].values - adata.var['variances_norm'] = df['variances_norm'].values.astype( - 'float64', copy=False - ) + adata.var['variances_norm'] = df['variances_norm'].values.astype('float64', copy=False) if batch_key is not None: - adata.var['highly_variable_nbatches'] = df[ - 'highly_variable_nbatches' - ].values + adata.var['highly_variable_nbatches'] = df['highly_variable_nbatches'].values if subset: adata._inplace_subset_var(df['highly_variable'].values) else: @@ -194,7 +181,6 @@ def _highly_variable_genes_single_batch( A DataFrame that contains the columns `highly_variable`, `means`, `dispersions`, and `dispersions_norm`. """ - X = adata.layers[layer] if layer is not None else adata.X if flavor == 'seurat': if 'log1p' in adata.uns_keys() and adata.uns['log1p']['base'] is not None: @@ -231,14 +217,11 @@ def _highly_variable_genes_single_batch( ) # Circumvent pandas 0.23 bug. Both sides of the assignment have dtype==float32, # but there’s still a dtype error without “.value”. - disp_std_bin[one_gene_per_bin.values] = disp_mean_bin[ - one_gene_per_bin.values - ].values + disp_std_bin[one_gene_per_bin.values] = disp_mean_bin[one_gene_per_bin.values].values disp_mean_bin[one_gene_per_bin.values] = 0 # actually do the normalization df['dispersions_norm'] = ( - df['dispersions'].values # use values here as index differs - - disp_mean_bin[df['mean_bin'].values].values + df['dispersions'].values - disp_mean_bin[df['mean_bin'].values].values # use values here as index differs ) / disp_std_bin[df['mean_bin'].values].values elif flavor == 'cell_ranger': from statsmodels import robust @@ -261,18 +244,13 @@ def _highly_variable_genes_single_batch( dispersion_norm = df['dispersions_norm'].values if n_top_genes is not None: dispersion_norm = dispersion_norm[~np.isnan(dispersion_norm)] - dispersion_norm[ - ::-1 - ].sort() # interestingly, np.argpartition is slightly slower + dispersion_norm[::-1].sort() # interestingly, np.argpartition is slightly slower if n_top_genes > adata.n_vars: - logg.info(f'`n_top_genes` > `adata.n_var`, returning all genes.') + logg.info('`n_top_genes` > `adata.n_var`, returning all genes.') n_top_genes = adata.n_vars disp_cut_off = dispersion_norm[n_top_genes - 1] gene_subset = np.nan_to_num(df['dispersions_norm'].values) >= disp_cut_off - logg.debug( - f'the {n_top_genes} top genes correspond to a ' - f'normalized dispersion cutoff of {disp_cut_off}' - ) + logg.debug(f'the {n_top_genes} top genes correspond to a ' f'normalized dispersion cutoff of {disp_cut_off}') else: dispersion_norm[np.isnan(dispersion_norm)] = 0 # similar to Seurat gene_subset = np.logical_and.reduce( @@ -400,9 +378,7 @@ def highly_variable_genes( This function replaces :func:`~scanpy.pp.filter_genes_dispersion`. """ - if n_top_genes is not None and not all( - m is None for m in [min_disp, max_disp, min_mean, max_mean] - ): + if n_top_genes is not None and not all(m is None for m in [min_disp, max_disp, min_mean, max_mean]): logg.info('If you pass `n_top_genes`, all cutoffs are ignored.') start = logg.info('extracting highly variable genes') @@ -487,12 +463,8 @@ def highly_variable_genes( highly_variable=np.nansum, ) ) - df.rename( - columns=dict(highly_variable='highly_variable_nbatches'), inplace=True - ) - df['highly_variable_intersection'] = df['highly_variable_nbatches'] == len( - batches - ) + df.rename(columns=dict(highly_variable='highly_variable_nbatches'), inplace=True) + df['highly_variable_intersection'] = df['highly_variable_nbatches'] == len(batches) if n_top_genes is not None: # sort genes by how often they selected as hvg within each batch and @@ -534,16 +506,10 @@ def highly_variable_genes( adata.var['highly_variable'] = df['highly_variable'].values adata.var['means'] = df['means'].values adata.var['dispersions'] = df['dispersions'].values - adata.var['dispersions_norm'] = df['dispersions_norm'].values.astype( - 'float32', copy=False - ) + adata.var['dispersions_norm'] = df['dispersions_norm'].values.astype('float32', copy=False) if batch_key is not None: - adata.var['highly_variable_nbatches'] = df[ - 'highly_variable_nbatches' - ].values - adata.var['highly_variable_intersection'] = df[ - 'highly_variable_intersection' - ].values + adata.var['highly_variable_nbatches'] = df['highly_variable_nbatches'].values + adata.var['highly_variable_intersection'] = df['highly_variable_intersection'].values if subset: adata._inplace_subset_var(df['highly_variable'].values) else: diff --git a/scanpy/preprocessing/_normalization.py b/scanpy/preprocessing/_normalization.py index 130c2e6109..9c2200aea4 100644 --- a/scanpy/preprocessing/_normalization.py +++ b/scanpy/preprocessing/_normalization.py @@ -131,9 +131,7 @@ def normalize_total( if layers == 'all': layers = adata.layers.keys() elif isinstance(layers, str): - raise ValueError( - f"`layers` needs to be a list of strings or 'all', not {layers!r}" - ) + raise ValueError(f"`layers` needs to be a list of strings or 'all', not {layers!r}") view_to_actual(adata) @@ -197,8 +195,6 @@ def normalize_total( time=start, ) if key_added is not None: - logg.debug( - f'and added {key_added!r}, counts per cell before normalization (adata.obs)' - ) + logg.debug(f'and added {key_added!r}, counts per cell before normalization (adata.obs)') return dat if not inplace else None diff --git a/scanpy/preprocessing/_pca.py b/scanpy/preprocessing/_pca.py index 9ab94cfae2..12a03b60a9 100644 --- a/scanpy/preprocessing/_pca.py +++ b/scanpy/preprocessing/_pca.py @@ -113,7 +113,7 @@ def pca( Explained variance, equivalent to the eigenvalues of the covariance matrix. """ - logg_start = logg.info(f'computing PCA') + logg_start = logg.info('computing PCA') # chunked calculation is not randomized, anyways if svd_solver in {'auto', 'randomized'} and not chunked: @@ -138,9 +138,7 @@ def pca( use_highly_variable = True if 'highly_variable' in adata.var.keys() else False if use_highly_variable: logg.info(' on highly variable genes') - adata_comp = ( - adata[:, adata.var['highly_variable']] if use_highly_variable else adata - ) + adata_comp = adata[:, adata.var['highly_variable']] if use_highly_variable else adata if n_comps is None: min_dim = min(adata_comp.n_vars, adata_comp.n_obs) @@ -182,9 +180,7 @@ def pca( "This may take a very large amount of memory." ) X = X.toarray() - pca_ = PCA( - n_components=n_comps, svd_solver=svd_solver, random_state=random_state - ) + pca_ = PCA(n_components=n_comps, svd_solver=svd_solver, random_state=random_state) X_pca = pca_.fit_transform(X) elif issparse(X) and zero_center: from sklearn.decomposition import PCA @@ -197,9 +193,7 @@ def pca( 'Use "arpack" (the default) or "lobpcg" instead.' ) - output = _pca_with_sparse( - X, n_comps, solver=svd_solver, random_state=random_state - ) + output = _pca_with_sparse(X, n_comps, solver=svd_solver, random_state=random_state) # this is just a wrapper for the results X_pca = output['X_pca'] pca_ = PCA(n_components=n_comps, svd_solver=svd_solver) @@ -215,9 +209,7 @@ def pca( ' the first component, e.g., might be heavily influenced by different means\n' ' the following components often resemble the exact PCA very closely' ) - pca_ = TruncatedSVD( - n_components=n_comps, random_state=random_state, algorithm=svd_solver - ) + pca_ = TruncatedSVD(n_components=n_comps, random_state=random_state, algorithm=svd_solver) X_pca = pca_.fit_transform(X) else: raise Exception("This shouldn't happen. Please open a bug report.") diff --git a/scanpy/preprocessing/_qc.py b/scanpy/preprocessing/_qc.py index 1f966f9d96..f915fe738c 100644 --- a/scanpy/preprocessing/_qc.py +++ b/scanpy/preprocessing/_qc.py @@ -24,8 +24,7 @@ def _choose_mtx_rep(adata, use_raw=False, layer=None): is_layer = layer is not None if use_raw and is_layer: raise ValueError( - "Cannot use expression from both layer and raw. You provided:" - f"'use_raw={use_raw}' and 'layer={layer}'" + "Cannot use expression from both layer and raw. You provided:" f"'use_raw={use_raw}' and 'layer={layer}'" ) if is_layer: return adata.layers[layer] @@ -103,33 +102,21 @@ def describe_obs( else: obs_metrics[f"n_{var_type}_by_{expr_type}"] = np.count_nonzero(X, axis=1) if log1p: - obs_metrics[f"log1p_n_{var_type}_by_{expr_type}"] = np.log1p( - obs_metrics[f"n_{var_type}_by_{expr_type}"] - ) + obs_metrics[f"log1p_n_{var_type}_by_{expr_type}"] = np.log1p(obs_metrics[f"n_{var_type}_by_{expr_type}"]) obs_metrics[f"total_{expr_type}"] = X.sum(axis=1) if log1p: - obs_metrics[f"log1p_total_{expr_type}"] = np.log1p( - obs_metrics[f"total_{expr_type}"] - ) + obs_metrics[f"log1p_total_{expr_type}"] = np.log1p(obs_metrics[f"total_{expr_type}"]) if percent_top: percent_top = sorted(percent_top) proportions = top_segment_proportions(X, percent_top) for i, n in enumerate(percent_top): - obs_metrics[f"pct_{expr_type}_in_top_{n}_{var_type}"] = ( - proportions[:, i] * 100 - ) + obs_metrics[f"pct_{expr_type}_in_top_{n}_{var_type}"] = proportions[:, i] * 100 for qc_var in qc_vars: - obs_metrics[f"total_{expr_type}_{qc_var}"] = X[:, adata.var[qc_var].values].sum( - axis=1 - ) + obs_metrics[f"total_{expr_type}_{qc_var}"] = X[:, adata.var[qc_var].values].sum(axis=1) if log1p: - obs_metrics[f"log1p_total_{expr_type}_{qc_var}"] = np.log1p( - obs_metrics[f"total_{expr_type}_{qc_var}"] - ) + obs_metrics[f"log1p_total_{expr_type}_{qc_var}"] = np.log1p(obs_metrics[f"total_{expr_type}_{qc_var}"]) obs_metrics[f"pct_{expr_type}_{qc_var}"] = ( - obs_metrics[f"total_{expr_type}_{qc_var}"] - / obs_metrics[f"total_{expr_type}"] - * 100 + obs_metrics[f"total_{expr_type}_{qc_var}"] / obs_metrics[f"total_{expr_type}"] * 100 ) if inplace: adata.obs[obs_metrics.columns] = obs_metrics @@ -193,17 +180,11 @@ def describe_var( var_metrics["n_cells_by_{expr_type}"] = np.count_nonzero(X, axis=0) var_metrics["mean_{expr_type}"] = X.mean(axis=0) if log1p: - var_metrics["log1p_mean_{expr_type}"] = np.log1p( - var_metrics["mean_{expr_type}"] - ) - var_metrics["pct_dropout_by_{expr_type}"] = ( - 1 - var_metrics["n_cells_by_{expr_type}"] / X.shape[0] - ) * 100 + var_metrics["log1p_mean_{expr_type}"] = np.log1p(var_metrics["mean_{expr_type}"]) + var_metrics["pct_dropout_by_{expr_type}"] = (1 - var_metrics["n_cells_by_{expr_type}"] / X.shape[0]) * 100 var_metrics["total_{expr_type}"] = np.ravel(X.sum(axis=0)) if log1p: - var_metrics["log1p_total_{expr_type}"] = np.log1p( - var_metrics["total_{expr_type}"] - ) + var_metrics["log1p_total_{expr_type}"] = np.log1p(var_metrics["total_{expr_type}"]) # Relabel new_colnames = [] for col in var_metrics.columns: @@ -377,9 +358,7 @@ def top_proportions_sparse_csr(data, indptr, n): return values -def top_segment_proportions( - mtx: Union[np.array, spmatrix], ns: Collection[int] -) -> np.ndarray: +def top_segment_proportions(mtx: Union[np.array, spmatrix], ns: Collection[int]) -> np.ndarray: """ Calculates total percentage of counts in top ns genes. @@ -403,15 +382,11 @@ def top_segment_proportions( return top_segment_proportions_dense(mtx, ns) -def top_segment_proportions_dense( - mtx: Union[np.array, spmatrix], ns: Collection[int] -) -> np.ndarray: +def top_segment_proportions_dense(mtx: Union[np.array, spmatrix], ns: Collection[int]) -> np.ndarray: # Currently ns is considered to be 1 indexed ns = np.sort(ns) sums = mtx.sum(axis=1) - partitioned = np.apply_along_axis(np.partition, 1, mtx, mtx.shape[1] - ns)[:, ::-1][ - :, : ns[-1] - ] + partitioned = np.apply_along_axis(np.partition, 1, mtx, mtx.shape[1] - ns)[:, ::-1][:, : ns[-1]] values = np.zeros((mtx.shape[0], len(ns))) acc = np.zeros(mtx.shape[0]) prev = 0 diff --git a/scanpy/preprocessing/_recipes.py b/scanpy/preprocessing/_recipes.py index d211bcc20a..8d6f43b364 100644 --- a/scanpy/preprocessing/_recipes.py +++ b/scanpy/preprocessing/_recipes.py @@ -47,9 +47,7 @@ def recipe_weinreb17( adata = adata.copy() if log: pp.log1p(adata) - adata.X = normalize_per_cell_weinreb16_deprecated( - adata.X, max_fraction=0.05, mult_with_mean=True - ) + adata.X = normalize_per_cell_weinreb16_deprecated(adata.X, max_fraction=0.05, mult_with_mean=True) gene_subset = filter_genes_cv_deprecated(adata.X, mean_threshold, cv_threshold) adata._inplace_subset_var(gene_subset) # this modifies the object itself X_pca = pp.pca( @@ -63,9 +61,7 @@ def recipe_weinreb17( return adata if copy else None -def recipe_seurat( - adata: AnnData, log: bool = True, plot: bool = False, copy: bool = False -) -> Optional[AnnData]: +def recipe_seurat(adata: AnnData, log: bool = True, plot: bool = False, copy: bool = False) -> Optional[AnnData]: """\ Normalization and filtering as of Seurat [Satija15]_. @@ -79,9 +75,7 @@ def recipe_seurat( pp.filter_cells(adata, min_genes=200) pp.filter_genes(adata, min_cells=3) normalize_total(adata, target_sum=1e4) - filter_result = filter_genes_dispersion( - adata.X, min_mean=0.0125, max_mean=3, min_disp=0.5, log=not log - ) + filter_result = filter_genes_dispersion(adata.X, min_mean=0.0125, max_mean=3, min_disp=0.5, log=not log) if plot: from ..plotting import ( _preprocessing as ppp, @@ -152,9 +146,7 @@ def recipe_zheng17( pp.filter_genes(adata, min_counts=1) # normalize with total UMI count per cell normalize_total(adata, key_added='n_counts_all') - filter_result = filter_genes_dispersion( - adata.X, flavor='cell_ranger', n_top_genes=n_top_genes, log=False - ) + filter_result = filter_genes_dispersion(adata.X, flavor='cell_ranger', n_top_genes=n_top_genes, log=False) if plot: # should not import at the top of the file from ..plotting import _preprocessing as ppp diff --git a/scanpy/preprocessing/_simple.py b/scanpy/preprocessing/_simple.py index eaa78dc689..70406533ec 100644 --- a/scanpy/preprocessing/_simple.py +++ b/scanpy/preprocessing/_simple.py @@ -119,9 +119,7 @@ def filter_cells( """ if copy: logg.warning('`copy` is deprecated, use `inplace` instead.') - n_given_options = sum( - option is not None for option in [min_genes, min_counts, max_genes, max_counts] - ) + n_given_options = sum(option is not None for option in [min_genes, min_counts, max_genes, max_counts]) if n_given_options != 1: raise ValueError( 'Only provide one of the optional parameters `min_counts`, ' @@ -143,9 +141,7 @@ def filter_cells( X = data # proceed with processing the data matrix min_number = min_counts if min_genes is None else min_genes max_number = max_counts if max_genes is None else max_genes - number_per_cell = np.sum( - X if min_genes is None and max_genes is None else X > 0, axis=1 - ) + number_per_cell = np.sum(X if min_genes is None and max_genes is None else X > 0, axis=1) if issparse(X): number_per_cell = number_per_cell.A1 if min_number is not None: @@ -158,18 +154,10 @@ def filter_cells( msg = f'filtered out {s} cells that have ' if min_genes is not None or min_counts is not None: msg += 'less than ' - msg += ( - f'{min_genes} genes expressed' - if min_counts is None - else f'{min_counts} counts' - ) + msg += f'{min_genes} genes expressed' if min_counts is None else f'{min_counts} counts' if max_genes is not None or max_counts is not None: msg += 'more than ' - msg += ( - f'{max_genes} genes expressed' - if max_counts is None - else f'{max_counts} counts' - ) + msg += f'{max_genes} genes expressed' if max_counts is None else f'{max_counts} counts' logg.info(msg) return cell_subset, number_per_cell @@ -223,9 +211,7 @@ def filter_genes( """ if copy: logg.warning('`copy` is deprecated, use `inplace` instead.') - n_given_options = sum( - option is not None for option in [min_cells, min_counts, max_cells, max_counts] - ) + n_given_options = sum(option is not None for option in [min_cells, min_counts, max_cells, max_counts]) if n_given_options != 1: raise ValueError( 'Only provide one of the optional parameters `min_counts`, ' @@ -255,9 +241,7 @@ def filter_genes( X = data # proceed with processing the data matrix min_number = min_counts if min_cells is None else min_cells max_number = max_counts if max_cells is None else max_cells - number_per_gene = np.sum( - X if min_cells is None and max_cells is None else X > 0, axis=0 - ) + number_per_gene = np.sum(X if min_cells is None and max_cells is None else X > 0, axis=0) if issparse(X): number_per_gene = number_per_gene.A1 if min_number is not None: @@ -270,14 +254,10 @@ def filter_genes( msg = f'filtered out {s} genes that are detected ' if min_cells is not None or min_counts is not None: msg += 'in less than ' - msg += ( - f'{min_cells} cells' if min_counts is None else f'{min_counts} counts' - ) + msg += f'{min_cells} cells' if min_counts is None else f'{min_counts} counts' if max_cells is not None or max_counts is not None: msg += 'in more than ' - msg += ( - f'{max_cells} cells' if max_counts is None else f'{max_counts} counts' - ) + msg += f'{max_cells} cells' if max_counts is None else f'{max_counts} counts' logg.info(msg) return gene_subset, number_per_gene @@ -323,17 +303,13 @@ def log1p( ------- Returns or updates `data`, depending on `copy`. """ - _check_array_function_arguments( - chunked=chunked, chunk_size=chunk_size, layer=layer, obsm=obsm - ) + _check_array_function_arguments(chunked=chunked, chunk_size=chunk_size, layer=layer, obsm=obsm) return log1p_array(X, copy=copy, base=base) @log1p.register(spmatrix) def log1p_sparse(X, *, base: Optional[Number] = None, copy: bool = False): - X = check_array( - X, accept_sparse=("csr", "csc"), dtype=(np.float64, np.float32), copy=copy - ) + X = check_array(X, accept_sparse=("csr", "csc"), dtype=(np.float64, np.float32), copy=copy) X.data = log1p(X.data, copy=False, base=base) return X @@ -347,9 +323,7 @@ def log1p_array(X, *, base: Optional[Number] = None, copy: bool = False): X = X.astype(np.floating) else: X = X.copy() - elif not ( - np.issubdtype(X.dtype, np.floating) or np.issubdtype(X.dtype, np.complex) - ): + elif not (np.issubdtype(X.dtype, np.floating) or np.issubdtype(X.dtype, np.complex)): X = X.astype(np.floating) np.log1p(X, out=X) if base is not None: @@ -376,9 +350,7 @@ def log1p_anndata( if chunked: if (layer is not None) or (obsm is not None): - raise NotImplementedError( - "Currently cannot perform chunked operations on arrays not stored in X." - ) + raise NotImplementedError("Currently cannot perform chunked operations on arrays not stored in X.") for chunk, start, end in adata.chunked_X(chunk_size): adata.X[start:end] = log1p(chunk, base=base, copy=False) else: @@ -520,9 +492,7 @@ def normalize_per_cell( start = logg.info('normalizing by total count per cell') adata = data.copy() if copy else data if counts_per_cell is None: - cell_subset, counts_per_cell = materialize_as_ndarray( - filter_cells(adata.X, min_counts=min_counts) - ) + cell_subset, counts_per_cell = materialize_as_ndarray(filter_cells(adata.X, min_counts=min_counts)) adata.obs[key_n_counts] = counts_per_cell adata._inplace_subset_obs(cell_subset) counts_per_cell = counts_per_cell[cell_subset] @@ -551,7 +521,7 @@ def normalize_per_cell( # proceed with data matrix X = data.copy() if copy else data if counts_per_cell is None: - if copy == False: + if not copy: raise ValueError('Can only be run with copy=True') cell_subset, counts_per_cell = filter_cells(X, min_counts=min_counts) X = X[cell_subset] @@ -703,9 +673,7 @@ def _regress_out_chunk(data): else: regres = regressors try: - result = sm.GLM( - data_chunk[:, col_index], regres, family=sm.families.Gaussian() - ).fit() + result = sm.GLM(data_chunk[:, col_index], regres, family=sm.families.Gaussian()).fit() new_column = result.resid_response except PerfectSeparationError: # this emulates R's behavior logg.warning('Encountered PerfectSeparationError, setting to 0 as in R.') @@ -757,7 +725,7 @@ def scale( annotated with `'mean'` and `'std'` in `adata.var`. """ _check_array_function_arguments(layer=layer, obsm=obsm) - return scale_array(data, zero_center=zero_center, max_value=max_value, copy=copy) + return scale_array(data, zero_center=zero_center, max_value=max_value, copy=copy) # noqa: F821 @scale.register(np.ndarray) @@ -777,10 +745,7 @@ def scale_array( ) if np.issubdtype(X.dtype, np.integer): - logg.info( - '... as scaling leads to float results, integer ' - 'input is cast to float, returning copy.' - ) + logg.info('... as scaling leads to float results, integer ' 'input is cast to float, returning copy.') X = X.astype(float) mean, var = _get_mean_var(X) @@ -817,10 +782,7 @@ def scale_sparse( ): # need to add the following here to make inplace logic work if zero_center: - logg.info( - "... as `zero_center=True`, sparse input is " - "densified and may lead to large memory consumption" - ) + logg.info("... as `zero_center=True`, sparse input is " "densified and may lead to large memory consumption") X = X.toarray() copy = False # Since the data has been copied return scale_array( @@ -954,9 +916,7 @@ def downsample_counts( total_counts_call = total_counts is not None counts_per_cell_call = counts_per_cell is not None if total_counts_call is counts_per_cell_call: - raise ValueError( - "Must specify exactly one of `total_counts` or `counts_per_cell`." - ) + raise ValueError("Must specify exactly one of `total_counts` or `counts_per_cell`.") if copy: adata = adata.copy() if total_counts_call: diff --git a/scanpy/preprocessing/_utils.py b/scanpy/preprocessing/_utils.py index 45ec781661..303a4d58d2 100644 --- a/scanpy/preprocessing/_utils.py +++ b/scanpy/preprocessing/_utils.py @@ -35,9 +35,7 @@ def sparse_mean_variance_axis(mtx: sparse.spmatrix, axis: int): else: raise ValueError("This function only works on sparse csr and csc matrices") if axis == ax_minor: - return sparse_mean_var_major_axis( - mtx.data, mtx.indices, mtx.indptr, *shape, np.float64 - ) + return sparse_mean_var_major_axis(mtx.data, mtx.indices, mtx.indptr, *shape, np.float64) else: return sparse_mean_var_minor_axis(mtx.data, mtx.indices, *shape, np.float64) diff --git a/scanpy/queries/_queries.py b/scanpy/queries/_queries.py index 7dff67565f..c206a5262e 100644 --- a/scanpy/queries/_queries.py +++ b/scanpy/queries/_queries.py @@ -60,13 +60,9 @@ def simple_query( try: from pybiomart import Server except ImportError: - raise ImportError( - "This method requires the `pybiomart` module to be installed." - ) + raise ImportError("This method requires the `pybiomart` module to be installed.") server = Server(host, use_cache=use_cache) - dataset = server.marts["ENSEMBL_MART_ENSEMBL"].datasets[ - "{}_gene_ensembl".format(org) - ] + dataset = server.marts["ENSEMBL_MART_ENSEMBL"].datasets["{}_gene_ensembl".format(org)] res = dataset.query(attributes=attrs, filters=filters, use_attr_names=True) return res @@ -264,16 +260,13 @@ def enrich( try: from gprofiler import GProfiler except ImportError: - raise ImportError( - "This method requires the `gprofiler-official` module to be installed." - ) + raise ImportError("This method requires the `gprofiler-official` module to be installed.") gprofiler = GProfiler(user_agent="scanpy", return_dataframe=True) gprofiler_kwargs = dict(gprofiler_kwargs) for k in ["organism"]: if gprofiler_kwargs.get(k) is not None: raise ValueError( - f"Argument `{k}` should be passed directly through `enrich`, " - "not through `gprofiler_kwargs`" + f"Argument `{k}` should be passed directly through `enrich`, " "not through `gprofiler_kwargs`" ) return gprofiler.profile(container, organism=org, **gprofiler_kwargs) diff --git a/scanpy/readwrite.py b/scanpy/readwrite.py index 96c9730f67..c31f8f042f 100644 --- a/scanpy/readwrite.py +++ b/scanpy/readwrite.py @@ -214,8 +214,7 @@ def _read_legacy_10x_h5(filename, *, genome=None, start=None): genome = children[0] elif genome not in children: raise ValueError( - f"Could not find genome '{genome}' in '{filename}'. " - f'Available genomes are: {children}' + f"Could not find genome '{genome}' in '{filename}'. " f'Available genomes are: {children}' ) dsets = {} for node in f.walk_nodes('/' + genome, 'Array'): @@ -373,26 +372,19 @@ def read_visium( for f in files.values(): if not f.exists(): if any(x in str(f) for x in ["hires_image", "lowres_image"]): - logg.warning( - f"You seem to be missing an image file.\n" - f"Could not find '{f}'." - ) + logg.warning(f"You seem to be missing an image file.\n" f"Could not find '{f}'.") else: raise OSError(f"Could not find '{f}'") adata.uns["spatial"][library_id]['images'] = dict() for res in ['hires', 'lowres']: try: - adata.uns["spatial"][library_id]['images'][res] = imread( - str(files[f'{res}_image']) - ) + adata.uns["spatial"][library_id]['images'][res] = imread(str(files[f'{res}_image'])) except Exception: raise OSError(f"Could not find '{res}_image'") # read json scalefactors - adata.uns["spatial"][library_id]['scalefactors'] = json.loads( - files['scalefactors_json_file'].read_bytes() - ) + adata.uns["spatial"][library_id]['scalefactors'] = json.loads(files['scalefactors_json_file'].read_bytes()) adata.uns["spatial"][library_id]["metadata"] = { k: (str(attrs[k], "utf-8") if isinstance(attrs[k], bytes) else attrs[k]) @@ -414,9 +406,7 @@ def read_visium( adata.obs = adata.obs.join(positions, how="left") - adata.obsm['spatial'] = adata.obs[ - ['pxl_row_in_fullres', 'pxl_col_in_fullres'] - ].to_numpy() + adata.obsm['spatial'] = adata.obs[['pxl_row_in_fullres', 'pxl_col_in_fullres']].to_numpy() adata.obs.drop( columns=['barcode', 'pxl_row_in_fullres', 'pxl_col_in_fullres'], inplace=True, @@ -426,9 +416,7 @@ def read_visium( if source_image_path is not None: # get an absolute path source_image_path = str(Path(source_image_path).resolve()) - adata.uns["spatial"][library_id]["metadata"]["source_image_path"] = str( - source_image_path - ) + adata.uns["spatial"][library_id]["metadata"]["source_image_path"] = str(source_image_path) return adata @@ -489,9 +477,7 @@ def read_10x_mtx( if genefile_exists or not gex_only: return adata else: - gex_rows = list( - map(lambda x: x == 'Gene Expression', adata.var['feature_types']) - ) + gex_rows = list(map(lambda x: x == 'Gene Expression', adata.var['feature_types'])) return adata[:, gex_rows].copy() @@ -560,9 +546,7 @@ def _read_v3_10x_mtx( else: raise ValueError("`var_names` needs to be 'gene_symbols' or 'gene_ids'") adata.var['feature_types'] = genes[2].values - adata.obs_names = pd.read_csv(path / f'{prefix}barcodes.tsv.gz', header=None)[ - 0 - ].values + adata.obs_names = pd.read_csv(path / f'{prefix}barcodes.tsv.gz', header=None)[0].values return adata @@ -612,9 +596,7 @@ def write( if ext == 'csv': adata.write_csvs(filename) else: - adata.write( - filename, compression=compression, compression_opts=compression_opts - ) + adata.write(filename, compression=compression, compression_opts=compression_opts) # ------------------------------------------------------------------------------- @@ -622,9 +604,7 @@ def write( # ------------------------------------------------------------------------------- -def read_params( - filename: Union[Path, str], asheader: bool = False -) -> Dict[str, Union[int, float, bool, str, None]]: +def read_params(filename: Union[Path, str], asheader: bool = False) -> Dict[str, Union[int, float, bool, str, None]]: """\ Read parameter dictionary from text file. @@ -699,9 +679,7 @@ def _read( **kwargs, ): if ext is not None and ext not in avail_exts: - raise ValueError( - 'Please provide one of the available extensions.\n' f'{avail_exts}' - ) + raise ValueError('Please provide one of the available extensions.\n' f'{avail_exts}') else: ext = is_valid_filename(filename, return_ext=True) is_present = _check_datafile_present_and_download(filename, backup_url=backup_url) @@ -715,9 +693,7 @@ def _read( logg.debug(f'reading sheet {sheet} from file {filename}') return read_hdf(filename, sheet) # read other file types - path_cache = settings.cachedir / _slugify(filename).replace( - '.' + ext, '.h5ad' - ) # type: Path + path_cache = settings.cachedir / _slugify(filename).replace('.' + ext, '.h5ad') # type: Path if path_cache.suffix in {'.gz', '.bz2'}: path_cache = path_cache.with_suffix('') if cache and path_cache.is_file(): @@ -756,10 +732,7 @@ def _read( else: raise ValueError(f'Unknown extension {ext}.') if cache: - logg.info( - f'... writing an {settings.file_format_data} ' - 'cache file to speedup reading next time' - ) + logg.info(f'... writing an {settings.file_format_data} ' 'cache file to speedup reading next time') if cache_compression is _empty: cache_compression = settings.cache_compression if not path_cache.parent.is_dir(): @@ -815,9 +788,9 @@ def _read_softgz(filename: Union[str, bytes, Path, BinaryIO]) -> AnnData: # Next line is the column headers (sample id's) sample_names = file.readline().strip().split("\t") # The column indices that contain gene expression data - I = [i for i, x in enumerate(sample_names) if x.startswith("GSM")] + indices = [i for i, x in enumerate(sample_names) if x.startswith("GSM")] # Restrict the column headers to those that we keep - sample_names = [sample_names[i] for i in I] + sample_names = [sample_names[i] for i in indices] # Get a list of sample labels groups = [samples_info[k] for k in sample_names] # Read the gene expression data as a list of lists, also get the gene @@ -831,7 +804,7 @@ def _read_softgz(filename: Union[str, bytes, Path, BinaryIO]) -> AnnData: V = line.split("\t") # Extract the values that correspond to gene expression measures # and convert the strings to numbers - x = [float(V[i]) for i in I] + x = [float(V[i]) for i in indices] X.append(x) gene_names.append(V[1]) # Convert the Python list of lists to a Numpy array and transpose to match @@ -903,9 +876,7 @@ def get_used_files(): """Get files used by processes with name scanpy.""" import psutil - loop_over_scanpy_processes = ( - proc for proc in psutil.process_iter() if proc.name() == 'scanpy' - ) + loop_over_scanpy_processes = (proc for proc in psutil.process_iter() if proc.name() == 'scanpy') filenames = [] for proc in loop_over_scanpy_processes: try: @@ -914,7 +885,7 @@ def get_used_files(): filenames.append(nt.path) # This catches a race condition where a process ends # before we can examine its files - except psutil.NoSuchProcess as err: + except psutil.NoSuchProcess: pass return set(filenames) @@ -967,10 +938,7 @@ def _check_datafile_present_and_download(path, backup_url=None): return True if backup_url is None: return False - logg.info( - f'try downloading from url\n{backup_url}\n' - '... this may take a while but only happens once' - ) + logg.info(f'try downloading from url\n{backup_url}\n' '... this may take a while but only happens once') if not path.parent.is_dir(): logg.info(f'creating directory {path.parent}/ for saving data') path.parent.mkdir(parents=True) @@ -985,8 +953,7 @@ def is_valid_filename(filename: Path, return_ext=False): if len(ext) > 2: logg.warning( - f'Your filename has more than two extensions: {ext}.\n' - f'Only considering the two last: {ext[-2:]}.' + f'Your filename has more than two extensions: {ext}.\n' f'Only considering the two last: {ext[-2:]}.' ) ext = ext[-2:] diff --git a/scanpy/tests/conftest.py b/scanpy/tests/conftest.py index bc9184f874..163cabeba0 100644 --- a/scanpy/tests/conftest.py +++ b/scanpy/tests/conftest.py @@ -1,14 +1,14 @@ +import scanpy +import pytest +from matplotlib.testing.compare import compare_images, make_test_filename +from matplotlib import pyplot import sys from pathlib import Path import matplotlib as mpl mpl.use('agg') -from matplotlib import pyplot -from matplotlib.testing.compare import compare_images, make_test_filename -import pytest -import scanpy scanpy.settings.verbosity = "hint" diff --git a/scanpy/tests/external/test_hashsolo.py b/scanpy/tests/external/test_hashsolo.py index 8ab8df0e61..4d3a223f28 100644 --- a/scanpy/tests/external/test_hashsolo.py +++ b/scanpy/tests/external/test_hashsolo.py @@ -23,9 +23,7 @@ def test_cell_demultiplexing(): sce.pp.hashsolo(test_data, test_data.obs.columns) doublets = ["Doublet"] * 10 - classes = list( - np.repeat(np.arange(10), 98).reshape(98, 10, order="F").ravel().astype(str) - ) + classes = list(np.repeat(np.arange(10), 98).reshape(98, 10, order="F").ravel().astype(str)) negatives = ["Negative"] * 10 classification = doublets + classes + negatives assert all(test_data.obs["Classification"].astype(str) == classification) diff --git a/scanpy/tests/external/test_wishbone.py b/scanpy/tests/external/test_wishbone.py index fc3cf71901..9baca5f877 100644 --- a/scanpy/tests/external/test_wishbone.py +++ b/scanpy/tests/external/test_wishbone.py @@ -20,6 +20,4 @@ def test_run_wishbone(): components=[2, 3], num_waypoints=150, ) - assert all( - [k in adata.obs for k in ['trajectory_wishbone', 'branch_wishbone']] - ), "Run Wishbone Error!" + assert all([k in adata.obs for k in ['trajectory_wishbone', 'branch_wishbone']]), "Run Wishbone Error!" diff --git a/scanpy/tests/helpers.py b/scanpy/tests/helpers.py index 35253bad69..dcb2f646d5 100644 --- a/scanpy/tests/helpers.py +++ b/scanpy/tests/helpers.py @@ -30,9 +30,7 @@ def check_rep_mutation(func, X, **kwargs): assert np.array_equal(asarray(adata_layer.X), asarray(adata_layer.obsm["obsm"])) assert np.array_equal(asarray(adata_obsm.X), asarray(adata_obsm.layers["layer"])) - assert np.array_equal( - asarray(adata_X.layers["layer"]), asarray(adata_X.obsm["obsm"]) - ) + assert np.array_equal(asarray(adata_X.layers["layer"]), asarray(adata_X.obsm["obsm"])) def check_rep_results(func, X, **kwargs): diff --git a/scanpy/tests/notebooks/test_paga_paul15_subsampled.py b/scanpy/tests/notebooks/test_paga_paul15_subsampled.py index 839d93f40a..38c3b4f7d3 100644 --- a/scanpy/tests/notebooks/test_paga_paul15_subsampled.py +++ b/scanpy/tests/notebooks/test_paga_paul15_subsampled.py @@ -3,6 +3,7 @@ # # This is the subsampled notebook for testing. +import scanpy as sc from pathlib import Path import numpy as np @@ -10,8 +11,6 @@ setup() -import scanpy as sc - HERE: Path = Path(__file__).parent ROOT = HERE / '_images_paga_paul15_subsampled' @@ -115,9 +114,7 @@ def test_paga_paul15_subsampled(image_comparer, plt): adata.obs['distance'] = adata.obs['dpt_pseudotime'] - _, axs = plt.subplots( - ncols=3, figsize=(6, 2.5), gridspec_kw={'wspace': 0.05, 'left': 0.12} - ) + _, axs = plt.subplots(ncols=3, figsize=(6, 2.5), gridspec_kw={'wspace': 0.05, 'left': 0.12}) plt.subplots_adjust(left=0.05, right=0.98, top=0.82, bottom=0.2) for ipath, (descr, path) in enumerate(paths): _, data = sc.pl.paga_path( diff --git a/scanpy/tests/notebooks/test_pbmc3k.py b/scanpy/tests/notebooks/test_pbmc3k.py index 0146c4b4de..42bd171986 100644 --- a/scanpy/tests/notebooks/test_pbmc3k.py +++ b/scanpy/tests/notebooks/test_pbmc3k.py @@ -10,6 +10,7 @@ # ([here](http://cf.10xgenomics.com/samples/cell-exp/1.1.0/pbmc3k/pbmc3k_filtered_gene_bc_matrices.tar.gz) # from this [webpage](https://support.10xgenomics.com/single-cell-gene-expression/datasets/1.1.0/pbmc3k)). +import scanpy as sc from pathlib import Path import numpy as np @@ -19,8 +20,6 @@ setup() -import scanpy as sc - HERE: Path = Path(__file__).parent ROOT = HERE / 'pbmc3k_images' @@ -30,9 +29,7 @@ def test_pbmc3k(image_comparer): save_and_compare_images = image_comparer(ROOT, FIGS, tol=20) - adata = sc.read( - './data/pbmc3k_raw.h5ad', backup_url='http://falexwolf.de/data/pbmc3k_raw.h5ad' - ) + adata = sc.read('./data/pbmc3k_raw.h5ad', backup_url='http://falexwolf.de/data/pbmc3k_raw.h5ad') # Preprocessing @@ -45,9 +42,7 @@ def test_pbmc3k(image_comparer): mito_genes = [name for name in adata.var_names if name.startswith('MT-')] # for each cell compute fraction of counts in mito genes vs. all genes # the `.A1` is only necessary as X is sparse to transform to a dense array after summing - adata.obs['percent_mito'] = ( - np.sum(adata[:, mito_genes].X, axis=1).A1 / np.sum(adata.X, axis=1).A1 - ) + adata.obs['percent_mito'] = np.sum(adata[:, mito_genes].X, axis=1).A1 / np.sum(adata.X, axis=1).A1 # add the total counts per cell as observations-annotation to adata adata.obs['n_counts'] = adata.X.sum(axis=1).A1 @@ -144,7 +139,5 @@ def test_pbmc3k(image_comparer): # sc.pl.umap(adata, color='louvain', legend_loc='on data', title='', frameon=False, show=False) # save_and_compare_images('umap_3') - sc.pl.violin( - adata, ['CST3', 'NKG7', 'PPBP'], groupby='louvain', rotation=90, show=False - ) + sc.pl.violin(adata, ['CST3', 'NKG7', 'PPBP'], groupby='louvain', rotation=90, show=False) save_and_compare_images('violin_2') diff --git a/scanpy/tests/test_combat.py b/scanpy/tests/test_combat.py index 295667b909..79be9a10ea 100644 --- a/scanpy/tests/test_combat.py +++ b/scanpy/tests/test_combat.py @@ -37,9 +37,7 @@ def test_covariates(): adata.obs['cat2'] = np.random.binomial(2, 0.1, size=(adata.n_obs)) adata.obs['num1'] = np.random.normal(size=(adata.n_obs)) - X2 = sc.pp.combat( - adata, key=key, covariates=['cat1', 'cat2', 'num1'], inplace=False - ) + X2 = sc.pp.combat(adata, key=key, covariates=['cat1', 'cat2', 'num1'], inplace=False) sc.pp.combat(adata, key=key, covariates=['cat1', 'cat2', 'num1'], inplace=True) assert X1.shape == X2.shape diff --git a/scanpy/tests/test_datasets.py b/scanpy/tests/test_datasets.py index 50332de6f9..30e3d497a8 100644 --- a/scanpy/tests/test_datasets.py +++ b/scanpy/tests/test_datasets.py @@ -63,10 +63,7 @@ def test_ebi_expression_atlas(tmp_dataset_dir): def test_krumsiek11(tmp_dataset_dir): adata = sc.datasets.krumsiek11() assert adata.shape == (640, 11) - assert all( - np.unique(adata.obs["cell_type"]) - == np.array(["Ery", "Mk", "Mo", "Neu", "progenitor"]) - ) + assert all(np.unique(adata.obs["cell_type"]) == np.array(["Ery", "Mk", "Mo", "Neu", "progenitor"])) def test_blobs(): @@ -102,18 +99,14 @@ def test_visium_datasets(tmp_dataset_dir, tmpdir): # Test that downloading tissue image works mbrain = sc.datasets.visium_sge("V1_Adult_Mouse_Brain", include_hires_tiff=True) expected_image_path = sc.settings.datasetdir / "V1_Adult_Mouse_Brain" / "image.tif" - image_path = Path( - mbrain.uns["spatial"]["V1_Adult_Mouse_Brain"]["metadata"]["source_image_path"] - ) + image_path = Path(mbrain.uns["spatial"]["V1_Adult_Mouse_Brain"]["metadata"]["source_image_path"]) assert image_path == expected_image_path # Test that tissue image exists and is a valid image file assert image_path.exists() # Test that tissue image is a tif image file (using `file`) - process = subprocess.run( - ['file', '--mime-type', image_path], stdout=subprocess.PIPE - ) + process = subprocess.run(['file', '--mime-type', image_path], stdout=subprocess.PIPE) output = process.stdout.strip().decode() # make process output string assert output == str(image_path) + ': image/tiff' diff --git a/scanpy/tests/test_docs.py b/scanpy/tests/test_docs.py index 0c82178be5..be826214b6 100644 --- a/scanpy/tests/test_docs.py +++ b/scanpy/tests/test_docs.py @@ -9,9 +9,7 @@ scanpy_functions = [ - c_or_f - for c_or_f in descend_classes_and_funcs(scanpy, "scanpy") - if isinstance(c_or_f, FunctionType) + c_or_f for c_or_f in descend_classes_and_funcs(scanpy, "scanpy") if isinstance(c_or_f, FunctionType) ] diff --git a/scanpy/tests/test_embedding_plots.py b/scanpy/tests/test_embedding_plots.py index 841ff63a8c..c1515d2ba2 100644 --- a/scanpy/tests/test_embedding_plots.py +++ b/scanpy/tests/test_embedding_plots.py @@ -29,9 +29,7 @@ def adata(): from sklearn.cluster import DBSCAN empty_pixel = np.array([1.0, 1.0, 1.0, 0]).reshape(1, 1, -1) - image = imread( - Path(sc.__file__).parent.parent / "docs/_static/img/Scanpy_Logo_RGB.png" - ) + image = imread(Path(sc.__file__).parent.parent / "docs/_static/img/Scanpy_Logo_RGB.png") x, y = np.where(np.logical_and.reduce(~np.equal(image, empty_pixel), axis=2)) # Just using to calculate the hex coords @@ -70,9 +68,7 @@ def adata(): adata.obs["label_missing"][::2] = np.nan adata.obs["1_missing"] = adata.obs_vector("1") - adata.obs.loc[ - adata.obsm["spatial"][:, 0] < adata.obsm["spatial"][:, 0].mean(), "1_missing" - ] = np.nan + adata.obs.loc[adata.obsm["spatial"][:, 0] < adata.obsm["spatial"][:, 0].mean(), "1_missing"] = np.nan return adata @@ -141,9 +137,7 @@ def test_missing_values_categorical( legend_loc, groupsfunc, ): - save_and_compare_images = image_comparer( - MISSING_VALUES_ROOT, MISSING_VALUES_FIGS, tol=15 - ) + save_and_compare_images = image_comparer(MISSING_VALUES_ROOT, MISSING_VALUES_FIGS, tol=15) base_name = fixture_request.node.name # Passing through a dict so it's easier to use default values @@ -159,12 +153,8 @@ def test_missing_values_categorical( save_and_compare_images(base_name) -def test_missing_values_continuous( - fixture_request, image_comparer, adata, plotfunc, na_color, legend_loc, vbounds -): - save_and_compare_images = image_comparer( - MISSING_VALUES_ROOT, MISSING_VALUES_FIGS, tol=15 - ) +def test_missing_values_continuous(fixture_request, image_comparer, adata, plotfunc, na_color, legend_loc, vbounds): + save_and_compare_images = image_comparer(MISSING_VALUES_ROOT, MISSING_VALUES_FIGS, tol=15) base_name = fixture_request.node.name # Passing through a dict so it's easier to use default values @@ -200,7 +190,7 @@ def test_enumerated_palettes(fixture_request, adata, tmpdir, plotfunc): check_images(dict_pth, list_pth, tol=15) -## Spatial specific +# Spatial specific def test_visium_circles(image_comparer): # standard visium data @@ -248,13 +238,9 @@ def test_spatial_general(image_comparer): # general coordinates save_and_compare_images = image_comparer(ROOT, FIGS, tol=15) adata = sc.read_visium(HERE / '_data' / 'visium_data' / '1.0.0') adata.obs = adata.obs.astype({'array_row': 'str'}) - spatial_metadata = adata.uns.pop( - "spatial" - ) # spatial data don't have imgs, so remove entry from uns + spatial_metadata = adata.uns.pop("spatial") # spatial data don't have imgs, so remove entry from uns # Required argument for now - spot_size = list(spatial_metadata.values())[0]["scalefactors"][ - "spot_diameter_fullres" - ] + spot_size = list(spatial_metadata.values())[0]["scalefactors"]["spot_diameter_fullres"] sc.pl.spatial(adata, show=False, spot_size=spot_size) save_and_compare_images('master_spatial_general_nocol') @@ -327,12 +313,8 @@ def equivalent_spatial_plotters_no_img(equivalent_spatial_plotters): pytest.param({"bw": True}, id="bw"), # Shape of the image for particular fixture, should not be hardcoded like this pytest.param({"img": np.ones((774, 1755, 4)), "scale_factor": 1.0}, id="img"), - pytest.param( - {"na_color": (0, 0, 0, 0), "color": "1_missing"}, id="na_color.transparent" - ), - pytest.param( - {"na_color": "lightgray", "color": "1_missing"}, id="na_color.lightgray" - ), + pytest.param({"na_color": (0, 0, 0, 0), "color": "1_missing"}, id="na_color.transparent"), + pytest.param({"na_color": "lightgray", "color": "1_missing"}, id="na_color.lightgray"), ] ) def spatial_kwargs(request): @@ -359,9 +341,7 @@ def test_manual_equivalency(equivalent_spatial_plotters, tmpdir, spatial_kwargs) check_images(orig_pth, removed_pth, tol=1) -def test_manual_equivalency_no_img( - equivalent_spatial_plotters_no_img, tmpdir, spatial_kwargs -): +def test_manual_equivalency_no_img(equivalent_spatial_plotters_no_img, tmpdir, spatial_kwargs): if "bw" in spatial_kwargs: # Has no meaning when there is no image pytest.skip() @@ -386,9 +366,7 @@ def test_white_background_vs_no_img(adata, tmpdir, spatial_kwargs): # These arguments don't make sense for this check pytest.skip() - white_background = np.ones_like( - adata.uns["spatial"]["scanpy_img"]["images"]["hires"] - ) + white_background = np.ones_like(adata.uns["spatial"]["scanpy_img"]["images"]["hires"]) TESTDIR = Path(tmpdir) white_pth = TESTDIR / "white_background.png" noimg_pth = TESTDIR / "no_img.png" @@ -412,9 +390,7 @@ def test_spatial_na_color(adata, tmpdir): """ Check that na_color defaults to transparent when an image is present, light gray when not. """ - white_background = np.ones_like( - adata.uns["spatial"]["scanpy_img"]["images"]["hires"] - ) + white_background = np.ones_like(adata.uns["spatial"]["scanpy_img"]["images"]["hires"]) TESTDIR = Path(tmpdir) lightgray_pth = TESTDIR / "lightgray.png" transparent_pth = TESTDIR / "transparent.png" diff --git a/scanpy/tests/test_filter_rank_genes_groups.py b/scanpy/tests/test_filter_rank_genes_groups.py index 91aff2d27c..9989a81989 100644 --- a/scanpy/tests/test_filter_rank_genes_groups.py +++ b/scanpy/tests/test_filter_rank_genes_groups.py @@ -47,9 +47,7 @@ def test_filter_rank_genes_groups(): 'max_out_group_fraction': 0.5, } - rank_genes_groups( - adata, 'bulk_labels', reference='Dendritic', method='wilcoxon', n_genes=5 - ) + rank_genes_groups(adata, 'bulk_labels', reference='Dendritic', method='wilcoxon', n_genes=5) filter_rank_genes_groups(**args) assert np.array_equal( diff --git a/scanpy/tests/test_get.py b/scanpy/tests/test_get.py index 7177fee921..1050e4ad55 100644 --- a/scanpy/tests/test_get.py +++ b/scanpy/tests/test_get.py @@ -39,12 +39,8 @@ def adata(): """ return AnnData( X=np.ones((2, 2)), - obs=pd.DataFrame( - {"obs1": [0, 1], "obs2": ["a", "b"]}, index=["cell1", "cell2"] - ), - var=pd.DataFrame( - {"gene_symbols": ["genesymbol1", "genesymbol2"]}, index=["gene1", "gene2"] - ), + obs=pd.DataFrame({"obs1": [0, 1], "obs2": ["a", "b"]}, index=["cell1", "cell2"]), + var=pd.DataFrame({"gene_symbols": ["genesymbol1", "genesymbol2"]}, index=["gene1", "gene2"]), layers={"double": sparse.csr_matrix(np.ones((2, 2)), dtype=int) * 2}, dtype=int, ) @@ -69,9 +65,7 @@ def test_obs_df(adata): dtype='float64', ) pd.testing.assert_frame_equal( - sc.get.obs_df( - adata, keys=["gene2", "obs1"], obsm_keys=[("eye", 0), ("sparse", 1)] - ), + sc.get.obs_df(adata, keys=["gene2", "obs1"], obsm_keys=[("eye", 0), ("sparse", 1)]), pd.DataFrame( {"gene2": [1, 1], "obs1": [0, 1], "eye-0": [1, 0], "sparse-1": [0.0, 1.0]}, index=adata.obs_names, @@ -361,9 +355,7 @@ def test_repeated_cols(dim, transform, func): adata = transform( sc.AnnData( np.ones((5, 10)), - obs=pd.DataFrame( - np.ones((5, 2)), columns=["a_column_name", "a_column_name"] - ), + obs=pd.DataFrame(np.ones((5, 2)), columns=["a_column_name", "a_column_name"]), var=pd.DataFrame(index=[f"gene-{i}" for i in range(10)]), ) ) @@ -380,9 +372,7 @@ def test_repeated_index_vals(dim, transform, func): adata = transform( sc.AnnData( np.ones((5, 10)), - var=pd.DataFrame( - index=["repeated_id"] * 2 + [f"gene-{i}" for i in range(8)] - ), + var=pd.DataFrame(index=["repeated_id"] * 2 + [f"gene-{i}" for i in range(8)]), ) ) diff --git a/scanpy/tests/test_highly_variable_genes.py b/scanpy/tests/test_highly_variable_genes.py index 8b47d5357e..6407571662 100644 --- a/scanpy/tests/test_highly_variable_genes.py +++ b/scanpy/tests/test_highly_variable_genes.py @@ -62,13 +62,9 @@ def test_higly_variable_genes_compare_to_seurat(): sc.pp.normalize_per_cell(pbmc, counts_per_cell_after=1e4) sc.pp.log1p(pbmc) - sc.pp.highly_variable_genes( - pbmc, flavor='seurat', min_mean=0.0125, max_mean=3, min_disp=0.5, inplace=True - ) + sc.pp.highly_variable_genes(pbmc, flavor='seurat', min_mean=0.0125, max_mean=3, min_disp=0.5, inplace=True) - np.testing.assert_array_equal( - seurat_hvg_info['highly_variable'], pbmc.var['highly_variable'] - ) + np.testing.assert_array_equal(seurat_hvg_info['highly_variable'], pbmc.var['highly_variable']) # (still) Not equal to tolerance rtol=2e-05, atol=2e-05 # np.testing.assert_allclose(4, 3.9999, rtol=2e-05, atol=2e-05) @@ -93,9 +89,7 @@ def test_higly_variable_genes_compare_to_seurat(): def test_higly_variable_genes_compare_to_seurat_v3(): - seurat_hvg_info = pd.read_csv( - FILE_V3, sep=' ', dtype={"variances_norm": np.float64} - ) + seurat_hvg_info = pd.read_csv(FILE_V3, sep=' ', dtype={"variances_norm": np.float64}) pbmc = sc.datasets.pbmc3k() pbmc.var_names_make_unique() @@ -106,9 +100,7 @@ def test_higly_variable_genes_compare_to_seurat_v3(): sc.pp.highly_variable_genes(pbmc, n_top_genes=1000, flavor='seurat_v3') sc.pp.highly_variable_genes(pbmc_dense, n_top_genes=1000, flavor='seurat_v3') - np.testing.assert_array_equal( - seurat_hvg_info['highly_variable'], pbmc.var['highly_variable'] - ) + np.testing.assert_array_equal(seurat_hvg_info['highly_variable'], pbmc.var['highly_variable']) np.testing.assert_allclose( seurat_hvg_info['variances'], pbmc.var['variances'], @@ -131,9 +123,7 @@ def test_higly_variable_genes_compare_to_seurat_v3(): batch = np.zeros((len(pbmc)), dtype=int) batch[1500:] = 1 pbmc.obs["batch"] = batch - df = sc.pp.highly_variable_genes( - pbmc, n_top_genes=4000, flavor='seurat_v3', batch_key="batch", inplace=False - ) + df = sc.pp.highly_variable_genes(pbmc, n_top_genes=4000, flavor='seurat_v3', batch_key="batch", inplace=False) df.sort_values( ["highly_variable_nbatches", "highly_variable_rank"], ascending=[False, True], @@ -141,9 +131,7 @@ def test_higly_variable_genes_compare_to_seurat_v3(): inplace=True, ) df = df.iloc[:4000] - seurat_hvg_info_batch = pd.read_csv( - FILE_V3_BATCH, sep=' ', dtype={"variances_norm": np.float64} - ) + seurat_hvg_info_batch = pd.read_csv(FILE_V3_BATCH, sep=' ', dtype={"variances_norm": np.float64}) # ranks might be slightly different due to many genes having same normalized var seu = pd.Index(seurat_hvg_info_batch['x'].values) @@ -168,9 +156,7 @@ def test_filter_genes_dispersion_compare_to_seurat(): min_disp=0.5, ) - np.testing.assert_array_equal( - seurat_hvg_info['highly_variable'], pbmc.var['highly_variable'] - ) + np.testing.assert_array_equal(seurat_hvg_info['highly_variable'], pbmc.var['highly_variable']) # (still) Not equal to tolerance rtol=2e-05, atol=2e-05: # np.testing.assert_allclose(4, 3.9999, rtol=2e-05, atol=2e-05) @@ -211,12 +197,8 @@ def test_highly_variable_genes_batches(): sc.pp.filter_genes(adata_1, min_cells=1) sc.pp.filter_genes(adata_2, min_cells=1) - hvg1 = sc.pp.highly_variable_genes( - adata_1, flavor='cell_ranger', n_top_genes=200, inplace=False - ) - hvg2 = sc.pp.highly_variable_genes( - adata_2, flavor='cell_ranger', n_top_genes=200, inplace=False - ) + hvg1 = sc.pp.highly_variable_genes(adata_1, flavor='cell_ranger', n_top_genes=200, inplace=False) + hvg2 = sc.pp.highly_variable_genes(adata_2, flavor='cell_ranger', n_top_genes=200, inplace=False) assert np.isclose( adata.var['dispersions_norm'][100], @@ -226,9 +208,7 @@ def test_highly_variable_genes_batches(): adata.var['dispersions_norm'][101], 0.5 * hvg1['dispersions_norm'][1] + 0.5 * hvg2['dispersions_norm'][101], ) - assert np.isclose( - adata.var['dispersions_norm'][0], 0.5 * hvg2['dispersions_norm'][0] - ) + assert np.isclose(adata.var['dispersions_norm'][0], 0.5 * hvg2['dispersions_norm'][0]) colnames = [ 'means', diff --git a/scanpy/tests/test_ingest.py b/scanpy/tests/test_ingest.py index a7ba765f98..bf3ad73605 100644 --- a/scanpy/tests/test_ingest.py +++ b/scanpy/tests/test_ingest.py @@ -137,9 +137,7 @@ def test_ingest_map_embedding_umap(): adata_ref = sc.AnnData(X) adata_new = sc.AnnData(T) - sc.pp.neighbors( - adata_ref, method='umap', use_rep='X', n_neighbors=4, random_state=0 - ) + sc.pp.neighbors(adata_ref, method='umap', use_rep='X', n_neighbors=4, random_state=0) sc.tl.umap(adata_ref, random_state=0) ing = sc.tl.Ingest(adata_ref) diff --git a/scanpy/tests/test_neighbors.py b/scanpy/tests/test_neighbors.py index 72df4fbf1a..468b02fdd4 100644 --- a/scanpy/tests/test_neighbors.py +++ b/scanpy/tests/test_neighbors.py @@ -143,13 +143,9 @@ def test_gauss_connectivities_euclidean(neigh): def test_metrics_argument(): no_knn_euclidean = get_neighbors() - no_knn_euclidean.compute_neighbors( - method="gauss", knn=False, n_neighbors=n_neighbors, metric="euclidean" - ) + no_knn_euclidean.compute_neighbors(method="gauss", knn=False, n_neighbors=n_neighbors, metric="euclidean") no_knn_manhattan = get_neighbors() - no_knn_manhattan.compute_neighbors( - method="gauss", knn=False, n_neighbors=n_neighbors, metric="manhattan" - ) + no_knn_manhattan.compute_neighbors(method="gauss", knn=False, n_neighbors=n_neighbors, metric="manhattan") assert not np.allclose(no_knn_euclidean.distances, no_knn_manhattan.distances) diff --git a/scanpy/tests/test_neighbors_key_added.py b/scanpy/tests/test_neighbors_key_added.py index 6793a40d15..db786e170c 100644 --- a/scanpy/tests/test_neighbors_key_added.py +++ b/scanpy/tests/test_neighbors_key_added.py @@ -19,12 +19,8 @@ def test_neighbors_key_added(adata): dists_key = adata.uns[key]['distances_key'] assert adata.uns['neighbors']['params'] == adata.uns[key]['params'] - assert np.allclose( - adata.obsp['connectivities'].toarray(), adata.obsp[conns_key].toarray() - ) - assert np.allclose( - adata.obsp['distances'].toarray(), adata.obsp[dists_key].toarray() - ) + assert np.allclose(adata.obsp['connectivities'].toarray(), adata.obsp[conns_key].toarray()) + assert np.allclose(adata.obsp['distances'].toarray(), adata.obsp[dists_key].toarray()) # test functions with neighbors_key and obsp diff --git a/scanpy/tests/test_pca.py b/scanpy/tests/test_pca.py index 36b6fab362..d645abbc03 100644 --- a/scanpy/tests/test_pca.py +++ b/scanpy/tests/test_pca.py @@ -94,9 +94,7 @@ def test_pca_sparse(pbmc3k_normalized): explicit = sc.pp.pca(pbmc_dense, dtype=np.float64, copy=True) assert np.allclose(implicit.uns["pca"]["variance"], explicit.uns["pca"]["variance"]) - assert np.allclose( - implicit.uns["pca"]["variance_ratio"], explicit.uns["pca"]["variance_ratio"] - ) + assert np.allclose(implicit.uns["pca"]["variance_ratio"], explicit.uns["pca"]["variance_ratio"]) assert np.allclose(implicit.obsm['X_pca'], explicit.obsm['X_pca']) assert np.allclose(implicit.varm['PCs'], explicit.varm['PCs']) @@ -127,13 +125,9 @@ def test_pca_chunked(pbmc3k_normalized): default = sc.pp.pca(pbmc3k_normalized, copy=True) # Taking absolute value since sometimes dimensions are flipped - np.testing.assert_allclose( - np.abs(chunked.obsm["X_pca"]), np.abs(default.obsm["X_pca"]) - ) + np.testing.assert_allclose(np.abs(chunked.obsm["X_pca"]), np.abs(default.obsm["X_pca"])) np.testing.assert_allclose(np.abs(chunked.varm["PCs"]), np.abs(default.varm["PCs"])) - np.testing.assert_allclose( - np.abs(chunked.uns["pca"]["variance"]), np.abs(default.uns["pca"]["variance"]) - ) + np.testing.assert_allclose(np.abs(chunked.uns["pca"]["variance"]), np.abs(default.uns["pca"]["variance"])) np.testing.assert_allclose( np.abs(chunked.uns["pca"]["variance_ratio"]), np.abs(default.uns["pca"]["variance_ratio"]), diff --git a/scanpy/tests/test_plotting.py b/scanpy/tests/test_plotting.py index 18bf345f42..69a15a5494 100644 --- a/scanpy/tests/test_plotting.py +++ b/scanpy/tests/test_plotting.py @@ -1,3 +1,11 @@ +import scanpy as sc +from anndata import AnnData +from matplotlib.testing.compare import compare_images +import pandas as pd +import numpy as np +import matplotlib.cm as cm +import matplotlib.pyplot as plt +import matplotlib as mpl from functools import partial from pathlib import Path from itertools import repeat, chain, combinations @@ -10,15 +18,6 @@ setup() -import matplotlib as mpl -import matplotlib.pyplot as plt -import matplotlib.cm as cm -import numpy as np -import pandas as pd -from matplotlib.testing.compare import compare_images -from anndata import AnnData - -import scanpy as sc HERE: Path = Path(__file__).parent ROOT = HERE / '_images' @@ -134,14 +133,10 @@ def test_heatmap(image_comparer): var=pd.DataFrame({"genes": 'g1 g2 g3'.split()}).set_index('genes'), ) a.obs['foo'] = a.obs['foo'].astype('category') - sc.pl.heatmap( - a, var_names=a.var_names, groupby='foo', swap_axes=True, figsize=(4, 4) - ) + sc.pl.heatmap(a, var_names=a.var_names, groupby='foo', swap_axes=True, figsize=(4, 4)) save_and_compare_images('master_heatmap_small_swap_alignment') - sc.pl.heatmap( - a, var_names=a.var_names, groupby='foo', swap_axes=False, figsize=(4, 4) - ) + sc.pl.heatmap(a, var_names=a.var_names, groupby='foo', swap_axes=False, figsize=(4, 4)) save_and_compare_images('master_heatmap_small_alignment') @@ -165,9 +160,7 @@ def test_clustermap(image_comparer, obs_keys, name): [ ( "dotplot", - partial( - sc.pl.dotplot, groupby='cell_type', title='dotplot', dendrogram=True - ), + partial(sc.pl.dotplot, groupby='cell_type', title='dotplot', dendrogram=True), ), ( "dotplot2", @@ -421,9 +414,7 @@ def test_tracksplot(image_comparer): save_and_compare_images = image_comparer(ROOT, FIGS, tol=15) adata = sc.datasets.krumsiek11() - sc.pl.tracksplot( - adata, adata.var_names, 'cell_type', dendrogram=True, use_raw=False - ) + sc.pl.tracksplot(adata, adata.var_names, 'cell_type', dendrogram=True, use_raw=False) save_and_compare_images('master_tracksplot') @@ -437,9 +428,7 @@ def test_multiple_plots(image_comparer): 'B-cell': ['CD79A', 'CD79B', 'MS4A1'], 'myeloid': ['CST3', 'LYZ'], } - fig, (ax1, ax2, ax3) = plt.subplots( - 1, 3, figsize=(20, 5), gridspec_kw={'wspace': 0.7} - ) + fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(20, 5), gridspec_kw={'wspace': 0.7}) __ = sc.pl.stacked_violin( adata, markers, @@ -560,9 +549,7 @@ def test_correlation(image_comparer): [ ( "ranked_genes_sharey", - partial( - sc.pl.rank_genes_groups, n_genes=12, n_panels_per_row=3, show=False - ), + partial(sc.pl.rank_genes_groups, n_genes=12, n_panels_per_row=3, show=False), ), ( "ranked_genes", @@ -576,9 +563,7 @@ def test_correlation(image_comparer): ), ( "ranked_genes_heatmap", - partial( - sc.pl.rank_genes_groups_heatmap, n_genes=4, cmap='YlGnBu', show=False - ), + partial(sc.pl.rank_genes_groups_heatmap, n_genes=4, cmap='YlGnBu', show=False), ), ( "ranked_genes_heatmap_swap_axes", @@ -755,9 +740,7 @@ def pbmc_scatterplots(): pytest.param( 'tsne', partial(sc.pl.tsne, color=['CD3D', 'louvain']), - marks=pytest.mark.xfail( - reason='slight differences even after setting random_state.' - ), + marks=pytest.mark.xfail(reason='slight differences even after setting random_state.'), ), ('umap_nocolor', sc.pl.umap), ( @@ -923,10 +906,7 @@ def test_scatter_rep(tmpdir): ), columns=["rep", "gene", "result"], ) - states["outpth"] = [ - TESTDIR / f"{state.gene}_{state.rep}_{state.result}.png" - for state in states.itertuples() - ] + states["outpth"] = [TESTDIR / f"{state.gene}_{state.rep}_{state.result}.png" for state in states.itertuples()] pattern = np.array(list(chain.from_iterable(repeat(i, 5) for i in range(3)))) coords = np.c_[np.arange(15) % 5, pattern] @@ -989,10 +969,7 @@ def test_paga(image_comparer): sc.pl.paga_compare(pbmc, basis='X_pca', legend_fontweight='normal', **common) save_and_compare_images('master_paga_compare_pca') - colors = { - c: {cm.Set1(_): 0.33 for _ in range(3)} - for c in pbmc.obs["bulk_labels"].cat.categories - } + colors = {c: {cm.Set1(_): 0.33 for _ in range(3)} for c in pbmc.obs["bulk_labels"].cat.categories} colors["Dendritic"] = {cm.Set2(_): 0.25 for _ in range(4)} sc.pl.paga(pbmc, color=colors, colorbar=False) diff --git a/scanpy/tests/test_preprocessing.py b/scanpy/tests/test_preprocessing.py index b9decb0579..c2167c4796 100644 --- a/scanpy/tests/test_preprocessing.py +++ b/scanpy/tests/test_preprocessing.py @@ -39,9 +39,7 @@ def base(request): def test_log1p_rep(count_matrix_format, base, dtype): - X = count_matrix_format( - np.abs(sp.random(100, 200, density=0.3, dtype=dtype)).toarray() - ) + X = count_matrix_format(np.abs(sp.random(100, 200, density=0.3, dtype=dtype)).toarray()) check_rep_mutation(sc.pp.log1p, X, base=base) check_rep_results(sc.pp.log1p, X, base=base) @@ -169,15 +167,11 @@ def test_regress_out_ordinal(): adata.obs['n_counts'] = adata.X.sum(axis=1) # results using only one processor - single = sc.pp.regress_out( - adata, keys=['n_counts', 'percent_mito'], n_jobs=1, copy=True - ) + single = sc.pp.regress_out(adata, keys=['n_counts', 'percent_mito'], n_jobs=1, copy=True) assert adata.X.shape == single.X.shape # results using 8 processors - multi = sc.pp.regress_out( - adata, keys=['n_counts', 'percent_mito'], n_jobs=8, copy=True - ) + multi = sc.pp.regress_out(adata, keys=['n_counts', 'percent_mito'], n_jobs=8, copy=True) np.testing.assert_array_equal(single.X, multi.X) @@ -255,15 +249,11 @@ def test_downsample_counts_per_cell(count_matrix_format, replace, dtype): X = X.astype(dtype) adata = AnnData(X=count_matrix_format(X), dtype=dtype) with pytest.raises(ValueError): - sc.pp.downsample_counts( - adata, counts_per_cell=TARGET, total_counts=TARGET, replace=replace - ) + sc.pp.downsample_counts(adata, counts_per_cell=TARGET, total_counts=TARGET, replace=replace) with pytest.raises(ValueError): sc.pp.downsample_counts(adata, replace=replace) initial_totals = np.ravel(adata.X.sum(axis=1)) - adata = sc.pp.downsample_counts( - adata, counts_per_cell=TARGET, replace=replace, copy=True - ) + adata = sc.pp.downsample_counts(adata, counts_per_cell=TARGET, replace=replace, copy=True) new_totals = np.ravel(adata.X.sum(axis=1)) if sp.issparse(adata.X): assert all(adata.X.toarray()[X == 0] == 0) @@ -271,17 +261,13 @@ def test_downsample_counts_per_cell(count_matrix_format, replace, dtype): assert all(adata.X[X == 0] == 0) assert all(new_totals <= TARGET) assert all(initial_totals >= new_totals) - assert all( - initial_totals[initial_totals <= TARGET] == new_totals[initial_totals <= TARGET] - ) + assert all(initial_totals[initial_totals <= TARGET] == new_totals[initial_totals <= TARGET]) if not replace: assert np.all(X >= adata.X) assert X.dtype == adata.X.dtype -def test_downsample_counts_per_cell_multiple_targets( - count_matrix_format, replace, dtype -): +def test_downsample_counts_per_cell_multiple_targets(count_matrix_format, replace, dtype): TARGETS = np.random.randint(500, 1500, 1000) X = np.random.randint(0, 100, (1000, 100)) * np.random.binomial(1, 0.3, (1000, 100)) X = X.astype(dtype) @@ -289,9 +275,7 @@ def test_downsample_counts_per_cell_multiple_targets( initial_totals = np.ravel(adata.X.sum(axis=1)) with pytest.raises(ValueError): sc.pp.downsample_counts(adata, counts_per_cell=[40, 10], replace=replace) - adata = sc.pp.downsample_counts( - adata, counts_per_cell=TARGETS, replace=replace, copy=True - ) + adata = sc.pp.downsample_counts(adata, counts_per_cell=TARGETS, replace=replace, copy=True) new_totals = np.ravel(adata.X.sum(axis=1)) if sp.issparse(adata.X): assert all(adata.X.toarray()[X == 0] == 0) @@ -299,10 +283,7 @@ def test_downsample_counts_per_cell_multiple_targets( assert all(adata.X[X == 0] == 0) assert all(new_totals <= TARGETS) assert all(initial_totals >= new_totals) - assert all( - initial_totals[initial_totals <= TARGETS] - == new_totals[initial_totals <= TARGETS] - ) + assert all(initial_totals[initial_totals <= TARGETS] == new_totals[initial_totals <= TARGETS]) if not replace: assert np.all(X >= adata.X) assert X.dtype == adata.X.dtype @@ -315,9 +296,7 @@ def test_downsample_total_counts(count_matrix_format, replace, dtype): total = X.sum() target = np.floor_divide(total, 10) initial_totals = np.ravel(adata_orig.X.sum(axis=1)) - adata = sc.pp.downsample_counts( - adata_orig, total_counts=target, replace=replace, copy=True - ) + adata = sc.pp.downsample_counts(adata_orig, total_counts=target, replace=replace, copy=True) new_totals = np.ravel(adata.X.sum(axis=1)) if sp.issparse(adata.X): assert all(adata.X.toarray()[X == 0] == 0) @@ -327,9 +306,7 @@ def test_downsample_total_counts(count_matrix_format, replace, dtype): assert all(initial_totals >= new_totals) if not replace: assert np.all(X >= adata.X) - adata = sc.pp.downsample_counts( - adata_orig, total_counts=total + 10, replace=False, copy=True - ) + adata = sc.pp.downsample_counts(adata_orig, total_counts=total + 10, replace=False, copy=True) assert (adata.X == X).all() assert X.dtype == adata.X.dtype diff --git a/scanpy/tests/test_preprocessing_distributed.py b/scanpy/tests/test_preprocessing_distributed.py index b7d91a063e..a550d90d7b 100644 --- a/scanpy/tests/test_preprocessing_distributed.py +++ b/scanpy/tests/test_preprocessing_distributed.py @@ -16,9 +16,7 @@ installed = {mod: bool(find_spec(mod)) for mod in required} -@pytest.mark.skipif( - not all(installed.values()), reason=f'{required} all required: {installed}' -) +@pytest.mark.skipif(not all(installed.values()), reason=f'{required} all required: {installed}') class TestPreprocessingDistributed: @pytest.fixture() def adata(self): diff --git a/scanpy/tests/test_qc_metrics.py b/scanpy/tests/test_qc_metrics.py index 71f6e728e0..33869de5a1 100644 --- a/scanpy/tests/test_qc_metrics.py +++ b/scanpy/tests/test_qc_metrics.py @@ -50,9 +50,7 @@ def test_segments_binary(): assert (segfull == propfull).all() -@pytest.mark.parametrize( - "cls", [np.asarray, sparse.csr_matrix, sparse.csc_matrix, sparse.coo_matrix] -) +@pytest.mark.parametrize("cls", [np.asarray, sparse.csr_matrix, sparse.csc_matrix, sparse.coo_matrix]) def test_top_segments(cls): a = cls(np.ones((300, 100))) seg = top_segment_proportions(a, [50, 100]) @@ -67,25 +65,16 @@ def test_top_segments(cls): # they’re also just making sure the metrics are there def test_qc_metrics(): adata = AnnData(X=sparse.csr_matrix(np.random.binomial(100, 0.005, (1000, 1000)))) - adata.var["mito"] = np.concatenate( - (np.ones(100, dtype=bool), np.zeros(900, dtype=bool)) - ) + adata.var["mito"] = np.concatenate((np.ones(100, dtype=bool), np.zeros(900, dtype=bool))) adata.var["negative"] = False sc.pp.calculate_qc_metrics(adata, qc_vars=["mito", "negative"], inplace=True) assert (adata.obs["n_genes_by_counts"] < adata.shape[1]).all() - assert ( - adata.obs["n_genes_by_counts"] >= adata.obs["log1p_n_genes_by_counts"] - ).all() + assert (adata.obs["n_genes_by_counts"] >= adata.obs["log1p_n_genes_by_counts"]).all() assert (adata.obs["total_counts"] == np.ravel(adata.X.sum(axis=1))).all() assert (adata.obs["total_counts"] >= adata.obs["log1p_total_counts"]).all() - assert ( - adata.obs["total_counts_mito"] >= adata.obs["log1p_total_counts_mito"] - ).all() + assert (adata.obs["total_counts_mito"] >= adata.obs["log1p_total_counts_mito"]).all() assert (adata.obs["total_counts_negative"] == 0).all() - assert ( - adata.obs["pct_counts_in_top_50_genes"] - <= adata.obs["pct_counts_in_top_100_genes"] - ).all() + assert (adata.obs["pct_counts_in_top_50_genes"] <= adata.obs["pct_counts_in_top_100_genes"]).all() for col in filter(lambda x: "negative" not in x, adata.obs.columns): assert (adata.obs[col] >= 0).all() # Values should be positive or zero assert (adata.obs[col] != 0).any().all() # Nothing should be all zeros @@ -108,29 +97,21 @@ def test_qc_metrics(): assert np.allclose(adata.var[col], old_var[col]) # with log1p=False adata = AnnData(X=sparse.csr_matrix(np.random.binomial(100, 0.005, (1000, 1000)))) - adata.var["mito"] = np.concatenate( - (np.ones(100, dtype=bool), np.zeros(900, dtype=bool)) - ) + adata.var["mito"] = np.concatenate((np.ones(100, dtype=bool), np.zeros(900, dtype=bool))) adata.var["negative"] = False - sc.pp.calculate_qc_metrics( - adata, qc_vars=["mito", "negative"], log1p=False, inplace=True - ) + sc.pp.calculate_qc_metrics(adata, qc_vars=["mito", "negative"], log1p=False, inplace=True) assert not np.any(adata.obs.columns.str.startswith("log1p_")) assert not np.any(adata.var.columns.str.startswith("log1p_")) def adata_mito(): a = np.random.binomial(100, 0.005, (1000, 1000)) - init_var = pd.DataFrame( - dict(mito=np.concatenate((np.ones(100, dtype=bool), np.zeros(900, dtype=bool)))) - ) + init_var = pd.DataFrame(dict(mito=np.concatenate((np.ones(100, dtype=bool), np.zeros(900, dtype=bool))))) adata_dense = AnnData(X=a, var=init_var.copy()) return adata_dense, init_var -@pytest.mark.parametrize( - "cls", [np.asarray, sparse.csr_matrix, sparse.csc_matrix, sparse.coo_matrix] -) +@pytest.mark.parametrize("cls", [np.asarray, sparse.csr_matrix, sparse.csc_matrix, sparse.coo_matrix]) def test_qc_metrics_format(cls): adata_dense, init_var = adata_mito() sc.pp.calculate_qc_metrics(adata_dense, qc_vars=["mito"], inplace=True) diff --git a/scanpy/tests/test_queries.py b/scanpy/tests/test_queries.py index e4ef9cc69d..c97e2e6767 100644 --- a/scanpy/tests/test_queries.py +++ b/scanpy/tests/test_queries.py @@ -20,9 +20,7 @@ def test_enrich(): sc.queries.enrich(pbmc, "1") gene_dict = {'set1': ['KLF4', 'PAX5'], 'set2': ['SOX2', 'NANOG']} - enrich_list = sc.queries.enrich( - gene_dict, org="hsapiens", gprofiler_kwargs=dict(sources=['GO:BP']) - ) + enrich_list = sc.queries.enrich(gene_dict, org="hsapiens", gprofiler_kwargs=dict(sources=['GO:BP'])) assert 'set1' in enrich_list['query'].unique() assert 'set2' in enrich_list['query'].unique() @@ -31,6 +29,4 @@ def test_enrich(): def test_mito_genes(): pbmc = sc.datasets.pbmc68k_reduced() mt_genes = sc.queries.mitochondrial_genes("hsapiens") - assert ( - pbmc.var_names.isin(mt_genes["external_gene_name"]).sum() == 1 - ) # Should only be MT-ND3 + assert pbmc.var_names.isin(mt_genes["external_gene_name"]).sum() == 1 # Should only be MT-ND3 diff --git a/scanpy/tests/test_rank_genes_groups.py b/scanpy/tests/test_rank_genes_groups.py index 1592a5c6a8..269adb4990 100644 --- a/scanpy/tests/test_rank_genes_groups.py +++ b/scanpy/tests/test_rank_genes_groups.py @@ -28,13 +28,9 @@ def get_example_data(*, sparse=False): # create test object - adata = AnnData( - np.multiply(binomial(1, 0.15, (100, 20)), negative_binomial(2, 0.25, (100, 20))) - ) + adata = AnnData(np.multiply(binomial(1, 0.15, (100, 20)), negative_binomial(2, 0.25, (100, 20)))) # adapt marker_genes for cluster (so as to have some form of reasonable input - adata.X[0:10, 0:5] = np.multiply( - binomial(1, 0.9, (10, 5)), negative_binomial(1, 0.5, (10, 5)) - ) + adata.X[0:10, 0:5] = np.multiply(binomial(1, 0.9, (10, 5)), negative_binomial(1, 0.5, (10, 5))) # The following construction is inefficient, but makes sure that the same data is used in the sparse case if sparse: @@ -81,30 +77,22 @@ def test_results_dense(): rank_genes_groups(adata, 'true_groups', n_genes=20, method='t-test') - adata.uns['rank_genes_groups']['names'] = adata.uns['rank_genes_groups'][ - 'names' - ].astype(true_names_t_test.dtype) + adata.uns['rank_genes_groups']['names'] = adata.uns['rank_genes_groups']['names'].astype(true_names_t_test.dtype) for name in true_scores_t_test.dtype.names: - assert np.allclose( - true_scores_t_test[name], adata.uns['rank_genes_groups']['scores'][name] - ) + assert np.allclose(true_scores_t_test[name], adata.uns['rank_genes_groups']['scores'][name]) assert np.array_equal(true_names_t_test, adata.uns['rank_genes_groups']['names']) rank_genes_groups(adata, 'true_groups', n_genes=20, method='wilcoxon') - adata.uns['rank_genes_groups']['names'] = adata.uns['rank_genes_groups'][ - 'names' - ].astype(true_names_wilcoxon.dtype) + adata.uns['rank_genes_groups']['names'] = adata.uns['rank_genes_groups']['names'].astype(true_names_wilcoxon.dtype) for name in true_scores_t_test.dtype.names: assert np.allclose( true_scores_wilcoxon[name][:7], adata.uns['rank_genes_groups']['scores'][name][:7], ) - assert np.array_equal( - true_names_wilcoxon[:7], adata.uns['rank_genes_groups']['names'][:7] - ) + assert np.array_equal(true_names_wilcoxon[:7], adata.uns['rank_genes_groups']['names'][:7]) def test_results_sparse(): @@ -121,30 +109,22 @@ def test_results_sparse(): rank_genes_groups(adata, 'true_groups', n_genes=20, method='t-test') - adata.uns['rank_genes_groups']['names'] = adata.uns['rank_genes_groups'][ - 'names' - ].astype(true_names_t_test.dtype) + adata.uns['rank_genes_groups']['names'] = adata.uns['rank_genes_groups']['names'].astype(true_names_t_test.dtype) for name in true_scores_t_test.dtype.names: - assert np.allclose( - true_scores_t_test[name], adata.uns['rank_genes_groups']['scores'][name] - ) + assert np.allclose(true_scores_t_test[name], adata.uns['rank_genes_groups']['scores'][name]) assert np.array_equal(true_names_t_test, adata.uns['rank_genes_groups']['names']) rank_genes_groups(adata, 'true_groups', n_genes=20, method='wilcoxon') - adata.uns['rank_genes_groups']['names'] = adata.uns['rank_genes_groups'][ - 'names' - ].astype(true_names_wilcoxon.dtype) + adata.uns['rank_genes_groups']['names'] = adata.uns['rank_genes_groups']['names'].astype(true_names_wilcoxon.dtype) for name in true_scores_t_test.dtype.names: assert np.allclose( true_scores_wilcoxon[name][:7], adata.uns['rank_genes_groups']['scores'][name][:7], ) - assert np.array_equal( - true_names_wilcoxon[:7], adata.uns['rank_genes_groups']['names'][:7] - ) + assert np.array_equal(true_names_wilcoxon[:7], adata.uns['rank_genes_groups']['names'][:7]) def test_results_layers(): @@ -235,11 +215,7 @@ def test_wilcoxon_symmetry(): rankby_abs=True, ) - stats_mono = ( - rank_genes_groups_df(pbmc, group="CD14+ Monocyte") - .drop(columns="names") - .to_numpy() - ) + stats_mono = rank_genes_groups_df(pbmc, group="CD14+ Monocyte").drop(columns="names").to_numpy() rank_genes_groups( pbmc, @@ -250,9 +226,7 @@ def test_wilcoxon_symmetry(): rankby_abs=True, ) - stats_dend = ( - rank_genes_groups_df(pbmc, group="Dendritic").drop(columns="names").to_numpy() - ) + stats_dend = rank_genes_groups_df(pbmc, group="Dendritic").drop(columns="names").to_numpy() assert np.allclose(np.abs(stats_mono), np.abs(stats_dend)) diff --git a/scanpy/tests/test_rank_genes_groups_logreg.py b/scanpy/tests/test_rank_genes_groups_logreg.py index f64a3c3fb7..a13997458e 100644 --- a/scanpy/tests/test_rank_genes_groups_logreg.py +++ b/scanpy/tests/test_rank_genes_groups_logreg.py @@ -34,9 +34,7 @@ def test_rank_genes_groups_with_renamed_categories_use_rep(): adata.X = adata.X[::-1, :] sc.tl.louvain(adata) - sc.tl.rank_genes_groups( - adata, 'louvain', method='logreg', layer="to_test", use_raw=False - ) + sc.tl.rank_genes_groups(adata, 'louvain', method='logreg', layer="to_test", use_raw=False) assert adata.uns['rank_genes_groups']['names'].dtype.names == ('0', '1', '2') assert adata.uns['rank_genes_groups']['names'][0].tolist() == ('3', '1', '0') diff --git a/scanpy/tests/test_read_10x.py b/scanpy/tests/test_read_10x.py index 6ed125ab4c..0ee8d7806c 100644 --- a/scanpy/tests/test_read_10x.py +++ b/scanpy/tests/test_read_10x.py @@ -67,9 +67,7 @@ def test_read_10x_h5_v1(): ROOT / '1.2.0' / 'filtered_gene_bc_matrices_h5.h5', genome='hg19_chr21', ) - nospec_genome_v1 = sc.read_10x_h5( - ROOT / '1.2.0' / 'filtered_gene_bc_matrices_h5.h5' - ) + nospec_genome_v1 = sc.read_10x_h5(ROOT / '1.2.0' / 'filtered_gene_bc_matrices_h5.h5') assert_anndata_equal(spec_genome_v1, nospec_genome_v1) @@ -113,6 +111,4 @@ def test_read_visium_counts(): def test_10x_h5_gex(): # Tests that gex option doesn't, say, make the function return None h5_pth = ROOT / '3.0.0' / 'filtered_feature_bc_matrix.h5' - assert_anndata_equal( - sc.read_10x_h5(h5_pth, gex_only=True), sc.read_10x_h5(h5_pth, gex_only=False) - ) + assert_anndata_equal(sc.read_10x_h5(h5_pth, gex_only=True), sc.read_10x_h5(h5_pth, gex_only=False)) diff --git a/scanpy/tests/test_scaling.py b/scanpy/tests/test_scaling.py index 9a0bf835b2..4eae8bae94 100644 --- a/scanpy/tests/test_scaling.py +++ b/scanpy/tests/test_scaling.py @@ -26,7 +26,7 @@ @pytest.mark.parametrize('typ', [np.array, csr_matrix], ids=lambda x: x.__name__) @pytest.mark.parametrize('dtype', ['float32', 'int64']) def test_scale(typ, dtype): - ## test AnnData arguments + # test AnnData arguments # test scaling with default zero_center == True adata0 = AnnData(typ(X), dtype=dtype) sc.pp.scale(adata0) @@ -39,7 +39,7 @@ def test_scale(typ, dtype): adata2 = AnnData(typ(X), dtype=dtype) sc.pp.scale(adata2, zero_center=False) assert np.allclose(csr_matrix(adata2.X).toarray(), X_scaled) - ## test bare count arguments, for simplicity only with explicit copy=True + # test bare count arguments, for simplicity only with explicit copy=True # test scaling with default zero_center == True data0 = typ(X, dtype=dtype) cdata0 = sc.pp.scale(data0, copy=True) diff --git a/scanpy/tests/test_score_genes.py b/scanpy/tests/test_score_genes.py index 9500031f66..497a72e0fc 100644 --- a/scanpy/tests/test_score_genes.py +++ b/scanpy/tests/test_score_genes.py @@ -9,12 +9,7 @@ def _create_random_gene_names(n_genes, name_length): """ creates a bunch of random gene names (just CAPS letters) """ - return np.array( - [ - ''.join(map(chr, np.random.randint(65, 90, name_length))) - for _ in range(n_genes) - ] - ) + return np.array([''.join(map(chr, np.random.randint(65, 90, name_length))) for _ in range(n_genes)]) def _create_sparse_nan_matrix(rows, cols, percent_zero, percent_nan): @@ -57,9 +52,7 @@ def test_add_score(): # the actual genes names are all 6letters # create some non-estinsting names with 7 letters: non_existing_genes = _create_random_gene_names(n_genes=3, name_length=7) - some_genes = np.r_[ - np.unique(np.random.choice(adata.var_names, 10)), np.unique(non_existing_genes) - ] + some_genes = np.r_[np.unique(np.random.choice(adata.var_names, 10)), np.unique(non_existing_genes)] sc.tl.score_genes(adata, some_genes, score_name='Test') assert adata.obs['Test'].dtype == 'float32' @@ -80,12 +73,8 @@ def test_sparse_nanmean(): # sparse matrix with nan S = _create_sparse_nan_matrix(R, C, percent_zero=0.3, percent_nan=0.3) - np.testing.assert_allclose( - np.nanmean(S.A, 1), np.array(_sparse_nanmean(S, 1)).flatten() - ) - np.testing.assert_allclose( - np.nanmean(S.A, 0), np.array(_sparse_nanmean(S, 0)).flatten() - ) + np.testing.assert_allclose(np.nanmean(S.A, 1), np.array(_sparse_nanmean(S, 1)).flatten()) + np.testing.assert_allclose(np.nanmean(S.A, 0), np.array(_sparse_nanmean(S, 0)).flatten()) # edge case of only NaNs per row A = np.full((10, 1), np.nan) @@ -118,9 +107,7 @@ def test_score_genes_sparse_vs_dense(): sc.tl.score_genes(adata_sparse, gene_list=gene_set, score_name='Test') sc.tl.score_genes(adata_dense, gene_list=gene_set, score_name='Test') - np.testing.assert_allclose( - adata_sparse.obs['Test'].values, adata_dense.obs['Test'].values - ) + np.testing.assert_allclose(adata_sparse.obs['Test'].values, adata_dense.obs['Test'].values) def test_score_genes_deplete(): diff --git a/scanpy/tools/_dendrogram.py b/scanpy/tools/_dendrogram.py index 788db127a6..7bcd67328c 100644 --- a/scanpy/tools/_dendrogram.py +++ b/scanpy/tools/_dendrogram.py @@ -109,16 +109,12 @@ def dendrogram( ) if var_names is None: - rep_df = pd.DataFrame( - _choose_representation(adata, use_rep=use_rep, n_pcs=n_pcs) - ) + rep_df = pd.DataFrame(_choose_representation(adata, use_rep=use_rep, n_pcs=n_pcs)) categorical = adata.obs[groupby[0]] if len(groupby) > 1: for group in groupby[1:]: # create new category by merging the given groupby categories - categorical = ( - categorical.astype(str) + "_" + adata.obs[group].astype(str) - ).astype('category') + categorical = (categorical.astype(str) + "_" + adata.obs[group].astype(str)).astype('category') categorical.name = "_".join(groupby) rep_df.set_index(categorical, inplace=True) @@ -137,9 +133,7 @@ def dendrogram( corr_matrix = mean_df.T.corr(method=cor_method) corr_condensed = distance.squareform(1 - corr_matrix) - z_var = sch.linkage( - corr_condensed, method=linkage_method, optimal_ordering=optimal_ordering - ) + z_var = sch.linkage(corr_condensed, method=linkage_method, optimal_ordering=optimal_ordering) dendro_info = sch.dendrogram(z_var, labels=list(categories), no_plot=True) dat = dict( diff --git a/scanpy/tools/_diffmap.py b/scanpy/tools/_diffmap.py index 9fda35e2a7..53cf5c2f22 100644 --- a/scanpy/tools/_diffmap.py +++ b/scanpy/tools/_diffmap.py @@ -57,9 +57,7 @@ def diffmap( neighbors_key = 'neighbors' if neighbors_key not in adata.uns: - raise ValueError( - 'You need to run `pp.neighbors` first to compute a neighborhood graph.' - ) + raise ValueError('You need to run `pp.neighbors` first to compute a neighborhood graph.') if n_comps <= 2: raise ValueError('Provide any value greater than 2 for `n_comps`. ') adata = adata.copy() if copy else adata diff --git a/scanpy/tools/_dpt.py b/scanpy/tools/_dpt.py index f2060eeb9b..4529cecda9 100644 --- a/scanpy/tools/_dpt.py +++ b/scanpy/tools/_dpt.py @@ -148,9 +148,7 @@ def dpt( logg.info(' this uses a hierarchical implementation') if dpt.iroot is not None: dpt._set_pseudotime() # pseudotimes are distances from root point - adata.uns[ - 'iroot' - ] = dpt.iroot # update iroot, might have changed when subsampling, for example + adata.uns['iroot'] = dpt.iroot # update iroot, might have changed when subsampling, for example adata.obs['dpt_pseudotime'] = dpt.pseudotime # detect branchings and partition the data into segments if n_branchings > 0: @@ -174,11 +172,7 @@ def dpt( time=start, deep=( 'added\n' - + ( - " 'dpt_pseudotime', the pseudotime (adata.obs)" - if dpt.iroot is not None - else '' - ) + + (" 'dpt_pseudotime', the pseudotime (adata.obs)" if dpt.iroot is not None else '') + ( "\n 'dpt_groups', the branching subgroups of dpt (adata.obs)" "\n 'dpt_order', cell order (adata.obs)" @@ -207,11 +201,7 @@ def __init__( super().__init__(adata, n_dcs=n_dcs, neighbors_key=neighbors_key) self.flavor = 'haghverdi16' self.n_branchings = n_branchings - self.min_group_size = ( - min_group_size - if min_group_size >= 1 - else int(min_group_size * self._adata.shape[0]) - ) + self.min_group_size = min_group_size if min_group_size >= 1 else int(min_group_size * self._adata.shape[0]) self.passed_adata = adata # just for debugging purposes self.choose_largest_segment = False self.allow_kendall_tau_shift = allow_kendall_tau_shift @@ -251,8 +241,7 @@ def detect_branchings(self): List of indices of the tips of segments. """ logg.debug( - f' detect {self.n_branchings} ' - f'branching{"" if self.n_branchings == 1 else "s"}', + f' detect {self.n_branchings} ' f'branching{"" if self.n_branchings == 1 else "s"}', ) # a segment is a subset of points of the data set (defined by the # indices of the points in the segment) @@ -282,11 +271,7 @@ def detect_branchings(self): # # let us define the tips of the whole data set if False: # this is safe, but not compatible with on-the-fly computation - tips_all = np.array( - np.unravel_index( - np.argmax(self.distances_dpt), self.distances_dpt.shape - ) - ) + tips_all = np.array(np.unravel_index(np.argmax(self.distances_dpt), self.distances_dpt.shape)) else: if self.iroot is not None: tip_0 = np.argmax(self.distances_dpt[self.iroot]) @@ -298,10 +283,7 @@ def detect_branchings(self): segs_connects = [[]] segs_undecided = [True] segs_adjacency = [[]] - logg.debug( - ' do not consider groups with less than ' - f'{self.min_group_size} points for splitting' - ) + logg.debug(' do not consider groups with less than ' f'{self.min_group_size} points for splitting') for ibranch in range(self.n_branchings): iseg, tips3 = self.select_segment(segs, segs_tips, segs_undecided) if iseg == -1: @@ -331,9 +313,7 @@ def detect_branchings(self): self.segs_connects[i, seg_adjacency] = segs_connects[i] for i in range(len(segs)): for j in range(len(segs)): - self.segs_adjacency[i, j] = self.distances_dpt[ - self.segs_connects[i, j], self.segs_connects[j, i] - ] + self.segs_adjacency[i, j] = self.distances_dpt[self.segs_connects[i, j], self.segs_connects[j, i]] self.segs_adjacency = self.segs_adjacency.tocsr() self.segs_connects = self.segs_connects.tocsr() @@ -342,15 +322,13 @@ def check_adjacency(self): for n_edges in range(1, np.max(n_edges_per_seg) + 1): for iseg in range(self.segs_adjacency.shape[0]): if n_edges_per_seg[iseg] == n_edges: - neighbor_segs = self.segs_adjacency[iseg].todense().A1 + _ = self.segs_adjacency[iseg].todense().A1 closest_points_other_segs = [ - seg[np.argmin(self.distances_dpt[self.segs_tips[iseg][0], seg])] - for seg in self.segs + seg[np.argmin(self.distances_dpt[self.segs_tips[iseg][0], seg])] for seg in self.segs ] seg = self.segs[iseg] closest_points_in_segs = [ - seg[np.argmin(self.distances_dpt[tips[0], seg])] - for tips in self.segs_tips + seg[np.argmin(self.distances_dpt[tips[0], seg])] for tips in self.segs_tips ] distance_segs = [ self.distances_dpt[closest_points_other_segs[ipoint], point] @@ -403,13 +381,8 @@ def select_segment(self, segs, segs_tips, segs_undecided) -> Tuple[int, int]: # take the inner tip, the "second tip" of the segment for itip in range(2): if ( - self.distances_dpt[ - segs_tips[jseg][1], segs_tips[iseg][itip] - ] - < 0.5 - * self.distances_dpt[ - segs_tips[iseg][~itip], segs_tips[iseg][itip] - ] + self.distances_dpt[segs_tips[jseg][1], segs_tips[iseg][itip]] + < 0.5 * self.distances_dpt[segs_tips[iseg][~itip], segs_tips[iseg][itip]] ): # logg.debug( # ' group', iseg, 'with tip', segs_tips[iseg][itip], @@ -443,9 +416,7 @@ def select_segment(self, segs, segs_tips, segs_undecided) -> Tuple[int, int]: # if we did not normalize, there would be a danger of simply # assigning the highest score to the longest segment score = dseg[tips3[2]] / Dseg[tips3[0], tips3[1]] - score = ( - len(seg) if self.choose_largest_segment else score - ) # simply the number of points + score = len(seg) if self.choose_largest_segment else score # simply the number of points logg.debug( f' group {iseg} score {score} n_points {len(seg)} ' + '(too small)' if len(seg) < self.min_group_size @@ -576,9 +547,7 @@ def detect_branching( segs_tips.insert(iseg, ssegs_tips[trunk]) # append other segments segs += [seg for iseg, seg in enumerate(ssegs) if iseg != trunk] - segs_tips += [ - seg_tips for iseg, seg_tips in enumerate(ssegs_tips) if iseg != trunk - ] + segs_tips += [seg_tips for iseg, seg_tips in enumerate(ssegs_tips) if iseg != trunk] if len(ssegs) == 4: # insert undecided cells at same position segs_undecided.pop(iseg) @@ -588,12 +557,8 @@ def detect_branching( prev_connecting_segments = segs_adjacency[iseg].copy() if self.flavor == 'haghverdi16': segs_adjacency += [[iseg] for i in range(n_add)] - segs_connects += [ - seg_connects - for iseg, seg_connects in enumerate(ssegs_connects) - if iseg != trunk - ] - prev_connecting_points = segs_connects[iseg] + segs_connects += [seg_connects for iseg, seg_connects in enumerate(ssegs_connects) if iseg != trunk] + _ = segs_connects[iseg] for jseg_cnt, jseg in enumerate(prev_connecting_segments): iseg_cnt = 0 for iseg_new, seg_new in enumerate(ssegs): @@ -610,9 +575,7 @@ def detect_branching( segs_connects[kseg].append(idx) break iseg_cnt += 1 - segs_adjacency[iseg] += list( - range(len(segs_adjacency) - n_add, len(segs_adjacency)) - ) + segs_adjacency[iseg] += list(range(len(segs_adjacency) - n_add, len(segs_adjacency))) segs_connects[iseg] += ssegs_connects[trunk] else: import networkx as nx @@ -628,28 +591,14 @@ def detect_branching( for kseg in kseg_list: reference_point_in_k = segs_tips[kseg][0] closest_points_in_jseg.append( - segs[jseg][ - np.argmin( - self.distances_dpt[reference_point_in_k, segs[jseg]] - ) - ] + segs[jseg][np.argmin(self.distances_dpt[reference_point_in_k, segs[jseg]])] ) # do not use the tip in the large segment j, instead, use the closest point - reference_point_in_j = closest_points_in_jseg[ - -1 - ] # segs_tips[jseg][0] + reference_point_in_j = closest_points_in_jseg[-1] # segs_tips[jseg][0] closest_points_in_kseg.append( - segs[kseg][ - np.argmin( - self.distances_dpt[reference_point_in_j, segs[kseg]] - ) - ] - ) - distances.append( - self.distances_dpt[ - closest_points_in_jseg[-1], closest_points_in_kseg[-1] - ] + segs[kseg][np.argmin(self.distances_dpt[reference_point_in_j, segs[kseg]])] ) + distances.append(self.distances_dpt[closest_points_in_jseg[-1], closest_points_in_kseg[-1]]) # print(jseg, '(', segs_tips[jseg][0], closest_points_in_jseg[-1], ')', # kseg, '(', segs_tips[kseg][0], closest_points_in_kseg[-1], ') :', distances[-1]) idx = np.argmin(distances) @@ -670,42 +619,22 @@ def detect_branching( distances = [] closest_points_in_jseg = [] closest_points_in_kseg = [] - jseg_list = [ - jseg - for jseg in range(len(segs)) - if jseg != kseg and jseg not in prev_connecting_segments - ] + jseg_list = [jseg for jseg in range(len(segs)) if jseg != kseg and jseg not in prev_connecting_segments] for jseg in jseg_list: reference_point_in_k = segs_tips[kseg][0] closest_points_in_jseg.append( - segs[jseg][ - np.argmin( - self.distances_dpt[reference_point_in_k, segs[jseg]] - ) - ] + segs[jseg][np.argmin(self.distances_dpt[reference_point_in_k, segs[jseg]])] ) # do not use the tip in the large segment j, instead, use the closest point - reference_point_in_j = closest_points_in_jseg[ - -1 - ] # segs_tips[jseg][0] + reference_point_in_j = closest_points_in_jseg[-1] # segs_tips[jseg][0] closest_points_in_kseg.append( - segs[kseg][ - np.argmin( - self.distances_dpt[reference_point_in_j, segs[kseg]] - ) - ] - ) - distances.append( - self.distances_dpt[ - closest_points_in_jseg[-1], closest_points_in_kseg[-1] - ] + segs[kseg][np.argmin(self.distances_dpt[reference_point_in_j, segs[kseg]])] ) + distances.append(self.distances_dpt[closest_points_in_jseg[-1], closest_points_in_kseg[-1]]) idx = np.argmin(distances) jseg_min = jseg_list[idx] if jseg_min not in kseg_list: - segs_adjacency_sparse = sp.sparse.lil_matrix( - (len(segs), len(segs)), dtype=float - ) + segs_adjacency_sparse = sp.sparse.lil_matrix((len(segs), len(segs)), dtype=float) for i, seg_adjacency in enumerate(segs_adjacency): segs_adjacency_sparse[i, seg_adjacency] = 1 G = nx.Graph(segs_adjacency_sparse) @@ -719,10 +648,7 @@ def detect_branching( # if we split the cluster, we should not attach kseg do_not_attach_kseg = True else: - logg.debug( - f' cannot attach new segment {kseg} at {jseg_min} ' - '(would produce cycle)' - ) + logg.debug(f' cannot attach new segment {kseg} at {jseg_min} ' '(would produce cycle)') if kseg != kseg_list[-1]: logg.debug(' continue') continue @@ -742,13 +668,7 @@ def _detect_branching( Dseg: np.ndarray, tips: np.ndarray, seg_reference=None, - ) -> Tuple[ - List[np.ndarray], - List[np.ndarray], - List[List[int]], - List[List[int]], - int, - ]: + ) -> Tuple[List[np.ndarray], List[np.ndarray], List[List[int]], List[List[int]], int]: """\ Detect branching on given segment. @@ -785,9 +705,7 @@ def _detect_branching( elif self.flavor == 'wolf17_bi' or self.flavor == 'wolf17_bi_un': ssegs = self._detect_branching_single_wolf17_bi(Dseg, tips) else: - raise ValueError( - '`flavor` needs to be in {"haghverdi16", "wolf17_tri", "wolf17_bi"}.' - ) + raise ValueError('`flavor` needs to be in {"haghverdi16", "wolf17_tri", "wolf17_bi"}.') # make sure that each data point has a unique association with a segment masks = np.zeros((len(ssegs), Dseg.shape[0]), dtype=bool) for iseg, seg in enumerate(ssegs): @@ -812,19 +730,13 @@ def _detect_branching( for inewseg, newseg_tips in enumerate(ssegs_tips): reference_point = newseg_tips[0] # closest cell to the new segment within undecided cells - closest_cell = undecided_cells[ - np.argmin(Dseg[reference_point][undecided_cells]) - ] + closest_cell = undecided_cells[np.argmin(Dseg[reference_point][undecided_cells])] ssegs_connects[inewseg].append(closest_cell) # closest cell to the undecided cells within new segment - closest_cell = ssegs[inewseg][ - np.argmin(Dseg[closest_cell][ssegs[inewseg]]) - ] + closest_cell = ssegs[inewseg][np.argmin(Dseg[closest_cell][ssegs[inewseg]])] ssegs_connects[-1].append(closest_cell) # also compute tips for the undecided cells - tip_0 = undecided_cells[ - np.argmax(Dseg[undecided_cells[0]][undecided_cells]) - ] + tip_0 = undecided_cells[np.argmax(Dseg[undecided_cells[0]][undecided_cells])] tip_1 = undecided_cells[np.argmax(Dseg[tip_0][undecided_cells])] ssegs_tips.append([tip_0, tip_1]) ssegs_adjacency = [[3], [3], [3], [0, 1, 2]] @@ -838,59 +750,35 @@ def _detect_branching( # this is another strategy than for the undecided_cells # here it's possible to use the more symmetric procedure # shouldn't make much of a difference - closest_points[0, 1] = ssegs[1][ - np.argmin(Dseg[reference_point[0]][ssegs[1]]) - ] - closest_points[1, 0] = ssegs[0][ - np.argmin(Dseg[reference_point[1]][ssegs[0]]) - ] - closest_points[0, 2] = ssegs[2][ - np.argmin(Dseg[reference_point[0]][ssegs[2]]) - ] - closest_points[2, 0] = ssegs[0][ - np.argmin(Dseg[reference_point[2]][ssegs[0]]) - ] - closest_points[1, 2] = ssegs[2][ - np.argmin(Dseg[reference_point[1]][ssegs[2]]) - ] - closest_points[2, 1] = ssegs[1][ - np.argmin(Dseg[reference_point[2]][ssegs[1]]) - ] + closest_points[0, 1] = ssegs[1][np.argmin(Dseg[reference_point[0]][ssegs[1]])] + closest_points[1, 0] = ssegs[0][np.argmin(Dseg[reference_point[1]][ssegs[0]])] + closest_points[0, 2] = ssegs[2][np.argmin(Dseg[reference_point[0]][ssegs[2]])] + closest_points[2, 0] = ssegs[0][np.argmin(Dseg[reference_point[2]][ssegs[0]])] + closest_points[1, 2] = ssegs[2][np.argmin(Dseg[reference_point[1]][ssegs[2]])] + closest_points[2, 1] = ssegs[1][np.argmin(Dseg[reference_point[2]][ssegs[1]])] added_dist = np.zeros(3) added_dist[0] = ( - Dseg[closest_points[1, 0], closest_points[0, 1]] - + Dseg[closest_points[2, 0], closest_points[0, 2]] + Dseg[closest_points[1, 0], closest_points[0, 1]] + Dseg[closest_points[2, 0], closest_points[0, 2]] ) added_dist[1] = ( - Dseg[closest_points[0, 1], closest_points[1, 0]] - + Dseg[closest_points[2, 1], closest_points[1, 2]] + Dseg[closest_points[0, 1], closest_points[1, 0]] + Dseg[closest_points[2, 1], closest_points[1, 2]] ) added_dist[2] = ( - Dseg[closest_points[1, 2], closest_points[2, 1]] - + Dseg[closest_points[0, 2], closest_points[2, 0]] + Dseg[closest_points[1, 2], closest_points[2, 1]] + Dseg[closest_points[0, 2], closest_points[2, 0]] ) trunk = np.argmin(added_dist) - ssegs_adjacency = [ - [trunk] if i != trunk else [j for j in range(3) if j != trunk] - for i in range(3) - ] + ssegs_adjacency = [[trunk] if i != trunk else [j for j in range(3) if j != trunk] for i in range(3)] ssegs_connects = [ - [closest_points[i, trunk]] - if i != trunk - else [closest_points[trunk, j] for j in range(3) if j != trunk] + [closest_points[i, trunk]] if i != trunk else [closest_points[trunk, j] for j in range(3) if j != trunk] for i in range(3) ] else: trunk = 0 ssegs_adjacency = [[1], [0]] reference_point_in_0 = ssegs_tips[0][0] - closest_point_in_1 = ssegs[1][ - np.argmin(Dseg[reference_point_in_0][ssegs[1]]) - ] + closest_point_in_1 = ssegs[1][np.argmin(Dseg[reference_point_in_0][ssegs[1]])] reference_point_in_1 = closest_point_in_1 # ssegs_tips[1][0] - closest_point_in_0 = ssegs[0][ - np.argmin(Dseg[reference_point_in_1][ssegs[0]]) - ] + closest_point_in_0 = ssegs[0][np.argmin(Dseg[reference_point_in_1][ssegs[0]])] ssegs_connects = [[closest_point_in_1], [closest_point_in_0]] return ssegs, ssegs_tips, ssegs_adjacency, ssegs_connects, trunk @@ -903,9 +791,9 @@ def _detect_branching_single_haghverdi16(self, Dseg, tips): # permutations of tip cells ps = [ [0, 1, 2], # start by computing distances from the first tip - [1, 2, 0], # -"- second tip + [1, 2, 0], # -"- second tip [2, 0, 1], - ] # -"- third tip + ] # -"- third tip for i, p in enumerate(ps): ssegs.append(self.__detect_branching_haghverdi16(Dseg, tips[p])) return ssegs @@ -940,9 +828,7 @@ def _detect_branching_single_wolf17_bi(self, Dseg, tips): ssegs = [closer_to_0_than_to_1, ~closer_to_0_than_to_1] return ssegs - def __detect_branching_haghverdi16( - self, Dseg: np.ndarray, tips: np.ndarray - ) -> np.ndarray: + def __detect_branching_haghverdi16(self, Dseg: np.ndarray, tips: np.ndarray) -> np.ndarray: """\ Detect branching on given segment. @@ -980,11 +866,9 @@ def __detect_branching_haghverdi16( # highly different, one would need to write the following equation # in terms of an ordering, such as exploited by the kendall # correlation method above - imax = np.argmin( - Dseg[tips[0]][idcs] + Dseg[tips[1]][idcs] + Dseg[tips[2]][idcs] - ) + imax = np.argmin(Dseg[tips[0]][idcs] + Dseg[tips[1]][idcs] + Dseg[tips[2]][idcs]) # init list to store new segments - ssegs = [] + ssegs = [] # noqa: F841 # first new segment: all points until, but excluding the branching point # increasing the following slightly from imax is a more conservative choice # as the criterion based on normalized distances, which follows below, diff --git a/scanpy/tools/_draw_graph.py b/scanpy/tools/_draw_graph.py index 0447880388..a8c55f3fa7 100644 --- a/scanpy/tools/_draw_graph.py +++ b/scanpy/tools/_draw_graph.py @@ -156,9 +156,7 @@ def draw_graph( iterations = kwds['iterations'] else: iterations = 500 - positions = forceatlas2.forceatlas2( - adjacency, pos=init_coords, iterations=iterations - ) + positions = forceatlas2.forceatlas2(adjacency, pos=init_coords, iterations=iterations) positions = np.array(positions) else: # igraph doesn't use numpy seed diff --git a/scanpy/tools/_embedding_density.py b/scanpy/tools/_embedding_density.py index b946837f2c..64c9794db6 100644 --- a/scanpy/tools/_embedding_density.py +++ b/scanpy/tools/_embedding_density.py @@ -119,8 +119,7 @@ def embedding_density( if f'X_{basis}' not in adata.obsm_keys(): raise ValueError( - "Cannot find the embedded representation " - f"`adata.obsm['X_{basis}']`. Compute the embedding first." + "Cannot find the embedded representation " f"`adata.obsm['X_{basis}']`. Compute the embedding first." ) if components is None: @@ -181,9 +180,7 @@ def embedding_density( if basis != 'diffmap': components += 1 - adata.uns[f'{density_covariate}_params'] = dict( - covariate=groupby, components=components.tolist() - ) + adata.uns[f'{density_covariate}_params'] = dict(covariate=groupby, components=components.tolist()) logg.hint( f"added\n" diff --git a/scanpy/tools/_ingest.py b/scanpy/tools/_ingest.py index 0941ded814..ed09731c48 100644 --- a/scanpy/tools/_ingest.py +++ b/scanpy/tools/_ingest.py @@ -113,12 +113,8 @@ def ingest( start = logg.info('running ingest') obs = [obs] if isinstance(obs, str) else obs - embedding_method = ( - [embedding_method] if isinstance(embedding_method, str) else embedding_method - ) - labeling_method = ( - [labeling_method] if isinstance(labeling_method, str) else labeling_method - ) + embedding_method = [embedding_method] if isinstance(embedding_method, str) else embedding_method + labeling_method = [labeling_method] if isinstance(labeling_method, str) else labeling_method if len(labeling_method) == 1 and len(obs or []) > 1: labeling_method = labeling_method * len(obs) @@ -251,9 +247,7 @@ def _init_dist_search(self, dist_args): make_initialized_nnd_search, ) - self._random_init, self._tree_init = make_initialisations( - dist_func, dist_args - ) + self._random_init, self._tree_init = make_initialisations(dist_func, dist_args) _initialise_search = partial( initialise_search, init_from_random=self._random_init, @@ -385,8 +379,7 @@ def __init__(self, adata, neighbors_key=None): self._init_neighbors(adata, neighbors_key) else: raise ValueError( - f'There is no neighbors data in `adata.uns["{neighbors_key}"]`.\n' - 'Please run pp.neighbors.' + f'There is no neighbors data in `adata.uns["{neighbors_key}"]`.\n' 'Please run pp.neighbors.' ) if 'X_umap' in adata.obsm: @@ -436,10 +429,7 @@ def fit(self, adata_new): new_var_names = adata_new.var_names.str.upper() if not ref_var_names.equals(new_var_names): - raise ValueError( - 'Variables in the new adata are different ' - 'from variables in the reference adata' - ) + raise ValueError('Variables in the new adata are different ' 'from variables in the reference adata') self._obs = pd.DataFrame(index=adata_new.obs.index) self._obsm = _DimDict(adata_new.n_obs, axis=0) @@ -473,13 +463,9 @@ def neighbors(self, k=None, queue_size=5, epsilon=0.1, random_state=0): else: from umap.utils import deheap_sort - init = self._initialise_search( - self._rp_forest, train, test, int(k * queue_size), rng_state=rng_state - ) + init = self._initialise_search(self._rp_forest, train, test, int(k * queue_size), rng_state=rng_state) - result = self._search( - train, self._search_graph.indptr, self._search_graph.indices, init, test - ) + result = self._search(train, self._search_graph.indptr, self._search_graph.indices, init, test) indices, dists = deheap_sort(result) self._indices, self._distances = indices[:, :k], dists[:, :k] @@ -499,14 +485,10 @@ def map_embedding(self, method): elif method == 'pca': self._obsm['X_pca'] = self._pca() else: - raise NotImplementedError( - 'Ingest supports only umap and pca embeddings for now.' - ) + raise NotImplementedError('Ingest supports only umap and pca embeddings for now.') def _knn_classify(self, labels): - cat_array = self._adata_ref.obs[labels].astype( - 'category' - ) # ensure it's categorical + cat_array = self._adata_ref.obs[labels].astype('category') # ensure it's categorical values = [cat_array[inds].mode()[0] for inds in self._indices] return pd.Categorical(values=values, categories=cat_array.cat.categories) @@ -542,9 +524,7 @@ def to_adata(self, inplace=False): if not inplace: return adata - def to_adata_joint( - self, batch_key='batch', batch_categories=None, index_unique='-' - ): + def to_adata_joint(self, batch_key='batch', batch_categories=None, index_unique='-'): """\ Returns concatenated object. @@ -565,14 +545,10 @@ def to_adata_joint( for key in self._obsm: if key in self._adata_ref.obsm: - adata.obsm[key] = np.vstack( - (self._adata_ref.obsm[key], self._obsm[key]) - ) + adata.obsm[key] = np.vstack((self._adata_ref.obsm[key], self._obsm[key])) if self._use_rep not in ('X_pca', 'X'): - adata.obsm[self._use_rep] = np.vstack( - (self._adata_ref.obsm[self._use_rep], self._obsm['rep']) - ) + adata.obsm[self._use_rep] = np.vstack((self._adata_ref.obsm[self._use_rep], self._obsm['rep'])) if 'X_umap' in self._obsm: adata.uns['umap'] = self._adata_ref.uns['umap'] diff --git a/scanpy/tools/_louvain.py b/scanpy/tools/_louvain.py index 9b09740284..d8597a7588 100644 --- a/scanpy/tools/_louvain.py +++ b/scanpy/tools/_louvain.py @@ -108,9 +108,7 @@ def louvain( partition_kwargs = dict(partition_kwargs) start = logg.info('running Louvain clustering') if (flavor != 'vtraag') and (partition_type is not None): - raise ValueError( - '`partition_type` is only a valid argument ' 'when `flavour` is "vtraag"' - ) + raise ValueError('`partition_type` is only a valid argument ' 'when `flavour` is "vtraag"') adata = adata.copy() if copy else adata if adjacency is None: adjacency = _choose_graph(adata, obsp, neighbors_key) @@ -182,12 +180,7 @@ def louvain( logg.info(' using the "louvain" package of rapids') louvain_parts, _ = cugraph.louvain(g) - groups = ( - louvain_parts.to_pandas() - .sort_values('vertex')[['partition']] - .to_numpy() - .ravel() - ) + groups = louvain_parts.to_pandas().sort_values('vertex')[['partition']].to_numpy().ravel() elif flavor == 'taynaud': # this is deprecated import networkx as nx diff --git a/scanpy/tools/_marker_gene_overlap.py b/scanpy/tools/_marker_gene_overlap.py index d50932216a..31b1d176dd 100644 --- a/scanpy/tools/_marker_gene_overlap.py +++ b/scanpy/tools/_marker_gene_overlap.py @@ -21,10 +21,7 @@ def _calc_overlap_count(markers1: dict, markers2: dict): overlaps = np.zeros((len(markers1), len(markers2))) for j, marker_group in enumerate(markers1): - tmp = [ - len(markers2[i].intersection(markers1[marker_group])) - for i in markers2.keys() - ] + tmp = [len(markers2[i].intersection(markers1[marker_group])) for i in markers2.keys()] overlaps[j, :] = tmp return overlaps @@ -59,8 +56,7 @@ def _calc_jaccard(markers1: dict, markers2: dict): for j, marker_group in enumerate(markers1): tmp = [ - len(markers2[i].intersection(markers1[marker_group])) - / len(markers2[i].union(markers1[marker_group])) + len(markers2[i].intersection(markers1[marker_group])) / len(markers2[i].union(markers1[marker_group])) for i in markers2.keys() ] jacc_results[j, :] = tmp @@ -158,15 +154,11 @@ def marker_gene_overlap( # Test user inputs if inplace: raise NotImplementedError( - 'Writing Pandas dataframes to h5ad is currently under development.' - '\nPlease use `inplace=False`.' + 'Writing Pandas dataframes to h5ad is currently under development.' '\nPlease use `inplace=False`.' ) if key not in adata.uns: - raise ValueError( - 'Could not find marker gene data. ' - 'Please run `sc.tl.rank_genes_groups()` first.' - ) + raise ValueError('Could not find marker gene data. ' 'Please run `sc.tl.rank_genes_groups()` first.') avail_methods = {'overlap_count', 'overlap_coef', 'jaccard', 'enrich'} if method not in avail_methods: @@ -184,14 +176,9 @@ def marker_gene_overlap( if not all(isinstance(val, cabc.Set) for val in reference_markers.values()): try: - reference_markers = { - key: set(val) for key, val in reference_markers.items() - } + reference_markers = {key: set(val) for key, val in reference_markers.items()} except Exception: - raise ValueError( - 'Please ensure that `reference_markers` contains ' - 'sets or lists of markers as values.' - ) + raise ValueError('Please ensure that `reference_markers` contains ' 'sets or lists of markers as values.') if adj_pval_threshold is not None: if 'pvals_adj' not in adata.uns[key]: @@ -202,26 +189,19 @@ def marker_gene_overlap( ) if adj_pval_threshold < 0: - logg.warning( - '`adj_pval_threshold` was set below 0. Threshold will be set to 0.' - ) + logg.warning('`adj_pval_threshold` was set below 0. Threshold will be set to 0.') adj_pval_threshold = 0 elif adj_pval_threshold > 1: - logg.warning( - '`adj_pval_threshold` was set above 1. Threshold will be set to 1.' - ) + logg.warning('`adj_pval_threshold` was set above 1. Threshold will be set to 1.') adj_pval_threshold = 1 if top_n_markers is not None: logg.warning( - 'Both `adj_pval_threshold` and `top_n_markers` is set. ' - '`adj_pval_threshold` will be ignored.' + 'Both `adj_pval_threshold` and `top_n_markers` is set. ' '`adj_pval_threshold` will be ignored.' ) if top_n_markers is not None and top_n_markers < 1: - logg.warning( - '`top_n_markers` was set below 1. `top_n_markers` will be set to 1.' - ) + logg.warning('`top_n_markers` was set below 1. `top_n_markers` will be set to 1.') top_n_markers = 1 # Get data-derived marker genes in a dictionary of sets @@ -249,16 +229,12 @@ def marker_gene_overlap( marker_match = _calc_overlap_count(reference_markers, data_markers) if normalize == 'reference': # Ensure rows sum to 1 - ref_lengths = np.array( - [len(reference_markers[m_group]) for m_group in reference_markers] - ) + ref_lengths = np.array([len(reference_markers[m_group]) for m_group in reference_markers]) marker_match = marker_match / ref_lengths[:, np.newaxis] marker_match = np.nan_to_num(marker_match) elif normalize == 'data': # Ensure columns sum to 1 - data_lengths = np.array( - [len(data_markers[dat_group]) for dat_group in data_markers] - ) + data_lengths = np.array([len(data_markers[dat_group]) for dat_group in data_markers]) marker_match = marker_match / data_lengths marker_match = np.nan_to_num(marker_match) elif method == 'overlap_coef': @@ -276,9 +252,7 @@ def marker_gene_overlap( # Create a pandas dataframe with the results marker_groups = list(reference_markers.keys()) clusters = list(cluster_ids) - marker_matching_df = pd.DataFrame( - marker_match, index=marker_groups, columns=clusters - ) + marker_matching_df = pd.DataFrame(marker_match, index=marker_groups, columns=clusters) # Store the results if inplace: diff --git a/scanpy/tools/_paga.py b/scanpy/tools/_paga.py index dea2f6f552..797fd12d5f 100644 --- a/scanpy/tools/_paga.py +++ b/scanpy/tools/_paga.py @@ -98,9 +98,7 @@ def paga( """ check_neighbors = 'neighbors' if neighbors_key is None else neighbors_key if check_neighbors not in adata.uns: - raise ValueError( - 'You need to run `pp.neighbors` first to compute a neighborhood graph.' - ) + raise ValueError('You need to run `pp.neighbors` first to compute a neighborhood graph.') if groups is None: for k in ("leiden", "louvain"): if k in adata.obs.columns: @@ -161,9 +159,7 @@ def compute_connectivities(self): elif self._model == 'v1.0': return self._compute_connectivities_v1_0() else: - raise ValueError( - f'`model` {self._model} needs to be one of {_AVAIL_MODELS}.' - ) + raise ValueError(f'`model` {self._model} needs to be one of {_AVAIL_MODELS}.') def _compute_connectivities_v1_2(self): import igraph @@ -172,9 +168,7 @@ def _compute_connectivities_v1_2(self): ones.data = np.ones(len(ones.data)) # should be directed if we deal with distances g = _utils.get_igraph_from_adjacency(ones, directed=True) - vc = igraph.VertexClustering( - g, membership=self._adata.obs[self._groups_key].cat.codes.values - ) + vc = igraph.VertexClustering(g, membership=self._adata.obs[self._groups_key].cat.codes.values) ns = vc.sizes() n = sum(ns) es_inner_cluster = [vc.subgraph(i).ecount() for i in range(len(ns))] @@ -208,9 +202,7 @@ def _compute_connectivities_v1_0(self): ones = self._neighbors.connectivities.copy() ones.data = np.ones(len(ones.data)) g = _utils.get_igraph_from_adjacency(ones) - vc = igraph.VertexClustering( - g, membership=self._adata.obs[self._groups_key].cat.codes.values - ) + vc = igraph.VertexClustering(g, membership=self._adata.obs[self._groups_key].cat.codes.values) ns = vc.sizes() cg = vc.cluster_graph(combine_edges='sum') inter_es = _utils.get_sparse_from_igraph(cg, weight_attr='weight') / 2 @@ -235,13 +227,8 @@ def _get_connectivities_tree_v1_2(self): inverse_connectivities = self.connectivities.copy() inverse_connectivities.data = 1.0 / inverse_connectivities.data connectivities_tree = minimum_spanning_tree(inverse_connectivities) - connectivities_tree_indices = [ - connectivities_tree[i].nonzero()[1] - for i in range(connectivities_tree.shape[0]) - ] - connectivities_tree = sp.sparse.lil_matrix( - self.connectivities.shape, dtype=float - ) + connectivities_tree_indices = [connectivities_tree[i].nonzero()[1] for i in range(connectivities_tree.shape[0])] + connectivities_tree = sp.sparse.lil_matrix(self.connectivities.shape, dtype=float) for i, neighbors in enumerate(connectivities_tree_indices): if len(neighbors) > 0: connectivities_tree[i, neighbors] = self.connectivities[i, neighbors] @@ -251,10 +238,7 @@ def _get_connectivities_tree_v1_0(self, inter_es): inverse_inter_es = inter_es.copy() inverse_inter_es.data = 1.0 / inverse_inter_es.data connectivities_tree = minimum_spanning_tree(inverse_inter_es) - connectivities_tree_indices = [ - connectivities_tree[i].nonzero()[1] - for i in range(connectivities_tree.shape[0]) - ] + connectivities_tree_indices = [connectivities_tree[i].nonzero()[1] for i in range(connectivities_tree.shape[0])] connectivities_tree = sp.sparse.lil_matrix(inter_es.shape, dtype=float) for i, neighbors in enumerate(connectivities_tree_indices): if len(neighbors) > 0: @@ -266,9 +250,7 @@ def compute_transitions(self): if vkey not in self._adata.uns: if 'velocyto_transitions' in self._adata.uns: self._adata.uns[vkey] = self._adata.uns['velocyto_transitions'] - logg.debug( - "The key 'velocyto_transitions' has been changed to 'velocity_graph'." - ) + logg.debug("The key 'velocyto_transitions' has been changed to 'velocity_graph'.") else: raise ValueError( 'The passed AnnData needs to have an `uns` annotation ' @@ -289,9 +271,7 @@ def compute_transitions(self): self._adata.uns[vkey].astype('bool'), directed=True, ) - vc = igraph.VertexClustering( - g, membership=self._adata.obs[self._groups_key].cat.codes.values - ) + vc = igraph.VertexClustering(g, membership=self._adata.obs[self._groups_key].cat.codes.values) # set combine_edges to False if you want self loops cg_full = vc.cluster_graph(combine_edges='sum') transitions = _utils.get_sparse_from_igraph(cg_full, weight_attr='weight') @@ -322,9 +302,7 @@ def compute_transitions_old(self): self._adata.uns['velocyto_transitions'], directed=True, ) - vc = igraph.VertexClustering( - g, membership=self._adata.obs[self._groups_key].cat.codes.values - ) + vc = igraph.VertexClustering(g, membership=self._adata.obs[self._groups_key].cat.codes.values) # this stores all single-cell edges in the cluster graph cg_full = vc.cluster_graph(combine_edges=False) # this is the boolean version that simply counts edges in the clustered graph @@ -332,9 +310,7 @@ def compute_transitions_old(self): self._adata.uns['velocyto_transitions'].astype('bool'), directed=True, ) - vc_bool = igraph.VertexClustering( - g_bool, membership=self._adata.obs[self._groups_key].cat.codes.values - ) + vc_bool = igraph.VertexClustering(g_bool, membership=self._adata.obs[self._groups_key].cat.codes.values) cg_bool = vc_bool.cluster_graph(combine_edges='sum') # collapsed version transitions = _utils.get_sparse_from_igraph(cg_bool, weight_attr='weight') total_n = self._neighbors.n_neighbors * np.array(vc_bool.sizes()) @@ -415,16 +391,12 @@ def paga_expression_entropies(adata) -> List[float]: """ from scipy.stats import entropy - groups_order, groups_masks = _utils.select_groups( - adata, key=adata.uns['paga']['groups'] - ) + groups_order, groups_masks = _utils.select_groups(adata, key=adata.uns['paga']['groups']) entropies = [] for mask in groups_masks: X_mask = adata.X[mask].todense() x_median = np.nanmedian(X_mask, axis=1, overwrite_input=True) - x_probs = (x_median - np.nanmin(x_median)) / ( - np.nanmax(x_median) - np.nanmin(x_median) - ) + x_probs = (x_median - np.nanmin(x_median)) / (np.nanmax(x_median) - np.nanmin(x_median)) entropies.append(entropy(x_probs)) return entropies @@ -478,11 +450,7 @@ def paga_compare_paths( import networkx as nx g1 = nx.Graph(adata1.uns['paga'][adjacency_key]) - g2 = nx.Graph( - adata2.uns['paga'][ - adjacency_key2 if adjacency_key2 is not None else adjacency_key - ] - ) + g2 = nx.Graph(adata2.uns['paga'][adjacency_key2 if adjacency_key2 is not None else adjacency_key]) leaf_nodes1 = [str(x) for x in g1.nodes() if g1.degree(x) == 1] logg.debug(f'leaf nodes in graph 1: {leaf_nodes1}') paga_groups = adata1.uns['paga']['groups'] @@ -534,25 +502,17 @@ def paga_compare_paths( n_steps += 1 continue if len(path1) >= len(path2): - path_mapped = [asso_groups1[l] for l in path1] + path_mapped = [asso_groups1[link] for link in path1] path_compare = path2 path_compare_id = 2 - path_compare_orig_names = [ - [orig_names2[int(s)] for s in l] for l in path_compare - ] - path_mapped_orig_names = [ - [orig_names2[int(s)] for s in l] for l in path_mapped - ] + path_compare_orig_names = [[orig_names2[int(s)] for s in link] for link in path_compare] + path_mapped_orig_names = [[orig_names2[int(s)] for s in link] for link in path_mapped] else: - path_mapped = [asso_groups2[l] for l in path2] + path_mapped = [asso_groups2[link] for link in path2] path_compare = path1 path_compare_id = 1 - path_compare_orig_names = [ - [orig_names1[int(s)] for s in l] for l in path_compare - ] - path_mapped_orig_names = [ - [orig_names1[int(s)] for s in l] for l in path_mapped - ] + path_compare_orig_names = [[orig_names1[int(s)] for s in link] for link in path_compare] + path_mapped_orig_names = [[orig_names1[int(s)] for s in link] for link in path_mapped] n_agreeing_steps_path = 0 ip_progress = 0 for il, l in enumerate(path_compare[:-1]): @@ -560,10 +520,7 @@ def paga_compare_paths( if ( ip < ip_progress or l not in p - or not ( - ip + 1 < len(path_mapped) - and path_compare[il + 1] in path_mapped[ip + 1] - ) + or not (ip + 1 < len(path_mapped) and path_compare[il + 1] in path_mapped[ip + 1]) ): continue # make sure that a step backward leads us to the same value of l @@ -581,8 +538,7 @@ def paga_compare_paths( # was ok in the previous step poss = list(range(ip - 1, ip_progress - 2, -1)) logg.debug( - f' step(s) backward to position(s) {poss} ' - 'in path_mapped are fine, too: valid step' + f' step(s) backward to position(s) {poss} ' 'in path_mapped are fine, too: valid step' ) n_agreeing_steps_path += 1 ip_progress = ip + 1 diff --git a/scanpy/tools/_rank_genes_groups.py b/scanpy/tools/_rank_genes_groups.py index 9f07c27eda..e096f5fd34 100644 --- a/scanpy/tools/_rank_genes_groups.py +++ b/scanpy/tools/_rank_genes_groups.py @@ -35,22 +35,32 @@ def _ranks(X, mask=None, mask_rest=None): n_genes = X.shape[1] if issparse(X): - merge = lambda tpl: vstack(tpl).toarray() - adapt = lambda X: X.toarray() + + def merge(tpl): + return vstack(tpl).toarray() + + def adapt(X): + return X.toarray() + else: merge = np.vstack - adapt = lambda X: X + + def adapt(X): + return X masked = mask is not None and mask_rest is not None if masked: n_cells = np.count_nonzero(mask) + np.count_nonzero(mask_rest) - get_chunk = lambda X, left, right: merge( - (X[mask, left:right], X[mask_rest, left:right]) - ) + + def get_chunk(X, left, right): + return merge((X[mask, left:right], X[mask_rest, left:right])) + else: n_cells = X.shape[0] - get_chunk = lambda X, left, right: adapt(X[:, left:right]) + + def get_chunk(X, left, right): + return adapt(X[:, left:right]) # Calculate chunk frames max_chunk = floor(CONST_MAX_SIZE / n_cells) @@ -94,9 +104,7 @@ def __init__( else: self.expm1_func = np.expm1 - self.groups_order, self.groups_masks = _utils.select_groups( - adata, groups, groupby - ) + self.groups_order, self.groups_masks = _utils.select_groups(adata, groups, groupby) # Singlet groups cause division by zero errors invalid_groups_selected = set(self.groups_order) & set( @@ -161,16 +169,19 @@ def _basic_stats(self): else: mask_rest = self.groups_masks[self.ireference] X_rest = self.X[mask_rest] - self.means[self.ireference], self.vars[self.ireference] = _get_mean_var( - X_rest - ) + self.means[self.ireference], self.vars[self.ireference] = _get_mean_var(X_rest) # deleting the next line causes a memory leak for some reason del X_rest if issparse(self.X): - get_nonzeros = lambda X: X.getnnz(axis=0) + + def get_nonzeros(X): + return X.getnnz(axis=0) + else: - get_nonzeros = lambda X: np.count_nonzero(X, axis=0) + + def get_nonzeros(X): + return np.count_nonzero(X, axis=0) for imask, mask in enumerate(self.groups_masks): X_mask = self.X[mask] @@ -269,10 +280,7 @@ def wilcoxon(self, tie_correct): m_active = np.count_nonzero(mask_rest) if n_active <= 25 or m_active <= 25: - logg.hint( - 'Few observations in a group for ' - 'normal approximation (<=25). Lower test accuracy.' - ) + logg.hint('Few observations in a group for ' 'normal approximation (<=25). Lower test accuracy.') # Calculate rank sums for each chunk for the current mask for ranks, left, right in _ranks(self.X, mask, mask_rest): @@ -280,13 +288,9 @@ def wilcoxon(self, tie_correct): if tie_correct: T[left:right] = _tiecorrect(ranks) - std_dev = np.sqrt( - T * n_active * m_active * (n_active + m_active + 1) / 12.0 - ) + std_dev = np.sqrt(T * n_active * m_active * (n_active + m_active + 1) / 12.0) - scores = ( - scores - (n_active * ((n_active + m_active + 1) / 2.0)) - ) / std_dev + scores = (scores - (n_active * ((n_active + m_active + 1) / 2.0))) / std_dev scores[np.isnan(scores)] = 0 pvals = 2 * stats.distributions.norm.sf(np.abs(scores)) @@ -316,13 +320,9 @@ def wilcoxon(self, tie_correct): else: T_i = 1 - std_dev = np.sqrt( - T_i * n_active * (n_cells - n_active) * (n_cells + 1) / 12.0 - ) + std_dev = np.sqrt(T_i * n_active * (n_cells - n_active) * (n_cells + 1) / 12.0) - scores[group_index, :] = ( - scores[group_index, :] - (n_active * (n_cells + 1) / 2.0) - ) / std_dev + scores[group_index, :] = (scores[group_index, :] - (n_active * (n_cells + 1) / 2.0)) / std_dev scores[np.isnan(scores)] = 0 pvals = 2 * stats.distributions.norm.sf(np.abs(scores[group_index, :])) @@ -400,9 +400,7 @@ def compute_statistics( from statsmodels.stats.multitest import multipletests pvals[np.isnan(pvals)] = 1 - _, pvals_adj, _, _ = multipletests( - pvals, alpha=0.05, method='fdr_bh' - ) + _, pvals_adj, _, _ = multipletests(pvals, alpha=0.05, method='fdr_bh') elif corr_method == 'bonferroni': pvals_adj = np.minimum(pvals * n_genes, 1.0) self.stats[group_name, 'pvals_adj'] = pvals_adj[global_indices] @@ -416,9 +414,7 @@ def compute_statistics( foldchanges = (self.expm1_func(mean_group) + 1e-9) / ( self.expm1_func(mean_rest) + 1e-9 ) # add small value to remove 0's - self.stats[group_name, 'logfoldchanges'] = np.log2( - foldchanges[global_indices] - ) + self.stats[group_name, 'logfoldchanges'] = np.log2(foldchanges[global_indices]) if n_genes_user is None: self.stats.index = self.var_names @@ -533,9 +529,7 @@ def rank_genes_groups( >>> sc.pl.rank_genes_groups(adata) """ if method is None: - logg.warning( - "Default of the method has been changed to 't-test' from 't-test_overestim_var'" - ) + logg.warning("Default of the method has been changed to 't-test' from 't-test_overestim_var'") method = 't-test' if 'only_positive' in kwds: @@ -565,9 +559,7 @@ def rank_genes_groups( groups_order += [reference] if reference != 'rest' and reference not in adata.obs[groupby].cat.categories: cats = adata.obs[groupby].cat.categories.tolist() - raise ValueError( - f'reference = {reference} needs to be one of groupby = {cats}.' - ) + raise ValueError(f'reference = {reference} needs to be one of groupby = {cats}.') if key_added is None: key_added = 'rank_genes_groups' @@ -593,15 +585,11 @@ def rank_genes_groups( logg.debug(f'consider {groupby!r} groups:') logg.debug(f'with sizes: {np.count_nonzero(test_obj.groups_masks, axis=1)}') - test_obj.compute_statistics( - method, corr_method, n_genes_user, rankby_abs, tie_correct, **kwds - ) + test_obj.compute_statistics(method, corr_method, n_genes_user, rankby_abs, tie_correct, **kwds) if test_obj.pts is not None: groups_names = [str(name) for name in test_obj.groups_order] - adata.uns[key_added]['pts'] = pd.DataFrame( - test_obj.pts.T, index=test_obj.var_names, columns=groups_names - ) + adata.uns[key_added]['pts'] = pd.DataFrame(test_obj.pts.T, index=test_obj.var_names, columns=groups_names) if test_obj.pts_rest is not None: adata.uns[key_added]['pts_rest'] = pd.DataFrame( test_obj.pts_rest.T, index=test_obj.var_names, columns=groups_names @@ -618,9 +606,7 @@ def rank_genes_groups( } for col in test_obj.stats.columns.levels[0]: - adata.uns[key_added][col] = test_obj.stats[col].to_records( - index=False, column_dtypes=dtypes[col] - ) + adata.uns[key_added][col] = test_obj.stats[col].to_records(index=False, column_dtypes=dtypes[col]) logg.info( ' finished', @@ -740,7 +726,10 @@ def filter_rank_genes_groups( ) if 'log1p' in adata.uns_keys() and adata.uns['log1p']['base'] is not None: - expm1_func = lambda x: np.expm1(x * np.log(adata.uns['log1p']['base'])) + + def expm1_func(x): + return np.expm1(x * np.log(adata.uns['log1p']['base'])) + else: expm1_func = np.expm1 @@ -762,12 +751,8 @@ def filter_rank_genes_groups( X_out = sub_X[~in_group] if use_fraction: - fraction_in_cluster_matrix.loc[:, cluster] = ( - adata.uns[key]['pts'][cluster].loc[var_names].values - ) - fraction_out_cluster_matrix.loc[:, cluster] = ( - adata.uns[key]['pts_rest'][cluster].loc[var_names].values - ) + fraction_in_cluster_matrix.loc[:, cluster] = adata.uns[key]['pts'][cluster].loc[var_names].values + fraction_out_cluster_matrix.loc[:, cluster] = adata.uns[key]['pts_rest'][cluster].loc[var_names].values else: fraction_in_cluster_matrix.loc[:, cluster] = _calc_frac(X_in) fraction_out_cluster_matrix.loc[:, cluster] = _calc_frac(X_out) @@ -778,8 +763,7 @@ def filter_rank_genes_groups( mean_out_cluster = np.ravel(X_out.mean(0)) # compute fold change fold_change_matrix.loc[:, cluster] = np.log2( - (expm1_func(mean_in_cluster) + 1e-9) - / (expm1_func(mean_out_cluster) + 1e-9) + (expm1_func(mean_in_cluster) + 1e-9) / (expm1_func(mean_out_cluster) + 1e-9) ) # filter original_matrix diff --git a/scanpy/tools/_score_genes.py b/scanpy/tools/_score_genes.py index d336544f0d..a02dfafa12 100644 --- a/scanpy/tools/_score_genes.py +++ b/scanpy/tools/_score_genes.py @@ -32,9 +32,7 @@ def _sparse_nanmean(X, axis): # the average s = Y.sum(axis) - m = s / n_elements.astype( - 'float32' - ) # if we dont cast the int32 to float32, this will result in float64... + m = s / n_elements.astype('float32') # if we dont cast the int32 to float32, this will result in float64... return m @@ -127,22 +125,16 @@ def score_genes( use_raw = _check_use_raw(adata, use_raw) _adata = adata.raw if use_raw else adata - _adata_subset = ( - _adata[:, gene_pool] if len(gene_pool) < len(_adata.var_names) else _adata - ) + _adata_subset = _adata[:, gene_pool] if len(gene_pool) < len(_adata.var_names) else _adata if issparse(_adata_subset.X): obs_avg = pd.Series( np.array(_sparse_nanmean(_adata_subset.X, axis=0)).flatten(), index=gene_pool, ) # average expression of genes else: - obs_avg = pd.Series( - np.nanmean(_adata_subset.X, axis=0), index=gene_pool - ) # average expression of genes + obs_avg = pd.Series(np.nanmean(_adata_subset.X, axis=0), index=gene_pool) # average expression of genes - obs_avg = obs_avg[ - np.isfinite(obs_avg) - ] # Sometimes (and I don't know how) missing data may be there, with nansfor + obs_avg = obs_avg[np.isfinite(obs_avg)] # Sometimes (and I don't know how) missing data may be there, with nansfor n_items = int(np.round(len(obs_avg) / (n_bins - 1))) obs_cut = obs_avg.rank(method='min') // n_items @@ -239,9 +231,7 @@ def score_genes_cell_cycle( adata = adata.copy() if copy else adata ctrl_size = min(len(s_genes), len(g2m_genes)) # add s-score - score_genes( - adata, gene_list=s_genes, score_name='S_score', ctrl_size=ctrl_size, **kwargs - ) + score_genes(adata, gene_list=s_genes, score_name='S_score', ctrl_size=ctrl_size, **kwargs) # add g2m-score score_genes( adata, diff --git a/scanpy/tools/_sim.py b/scanpy/tools/_sim.py index 98e88f7d70..e2e4fcbb68 100644 --- a/scanpy/tools/_sim.py +++ b/scanpy/tools/_sim.py @@ -164,9 +164,7 @@ def sample_dynamic_data(**params): for restart in range(nrRealizations + maxRestarts): # slightly break symmetry in initial conditions if 'toggleswitch' in model_key: - X0 = np.array( - [0.8 for i in range(grnsim.dim)] - ) + 0.01 * np.random.randn(grnsim.dim) + X0 = np.array([0.8 for i in range(grnsim.dim)]) + 0.01 * np.random.randn(grnsim.dim) X = grnsim.sim_model(tmax=tmax, X0=X0, noiseDyn=noiseDyn) # check branching check = True @@ -207,7 +205,6 @@ def sample_dynamic_data(**params): step = 5 grnsim = GRNsim(dim=dim, initType=initType, model=model_key, params=params) - curr_nrSamples = 0 Xsamples = [] for sample in range(maxNrSamples): # choose initial conditions such that branchings result @@ -262,9 +259,7 @@ def sample_dynamic_data(**params): for filename in writedir.glob('sim*.txt'): pass logg.info(f'reading simulation results {filename}') - adata = readwrite._read( - filename, first_column_names=True, suppress_cache_warning=True - ) + adata = readwrite._read(filename, first_column_names=True, suppress_cache_warning=True) adata.uns['tmax_write'] = tmax / step return adata @@ -312,16 +307,12 @@ def write_data( Adj[i, i] = 1 np.savetxt(dir + '/adj_' + id + '.txt', Adj, header=header, fmt='%d') if Coupl.size > 0: - np.savetxt( - dir + '/coupl_' + id + '.txt', Coupl, header=header, fmt='%10.6f' - ) + np.savetxt(dir + '/coupl_' + id + '.txt', Coupl, header=header, fmt='%10.6f') # write model file if varNames and Coupl.size > 0: with (dir / f'model_{id}.txt').open('w') as f: f.write('# For each "variable = ", there must be a right hand side: \n') - f.write( - '# either an empty string or a python-style logical expression \n' - ) + f.write('# either an empty string or a python-style logical expression \n') f.write('# involving variable names, "or", "and", "(", ")". \n') f.write('# The order of equations matters! \n') f.write('# \n') @@ -337,11 +328,7 @@ def write_data( for gp in range(dim): for g in range(dim): if np.abs(Coupl[gp, g]) > 1e-10: - f.write( - f'{names[gp]:10} ' - f'{names[g]:10} ' - f'{Coupl[gp, g]:10.3} \n' - ) + f.write(f'{names[gp]:10} ' f'{names[g]:10} ' f'{Coupl[gp, g]:10.3} \n') # write simulated data # the binary mode option in the following line is a fix for python 3 # variable names @@ -397,9 +384,7 @@ def __init__( either string for predefined model, or directory with a model file and a couple matrix files """ - self.dim = ( - dim if Coupl is None else Coupl.shape[0] - ) # number of nodes / dimension of system + self.dim = dim if Coupl is None else Coupl.shape[0] # number of nodes / dimension of system self.maxnpar = 1 # maximal number of parents self.p_indep = 0.4 # fraction of independent genes self.model = model @@ -410,9 +395,8 @@ def __init__( # checks if initType not in ['branch', 'random']: raise RuntimeError('initType must be either: branch, random') - read = False if model not in self.availModels.keys(): - message = 'model not among predefined models \n' + message = 'model not among predefined models \n' # noqa: F841 # read from file from .. import sim_models @@ -477,24 +461,16 @@ def Xdiff_hill(self, Xt): iparent = self.varNames[self.pas[child][iv]] x = Xt[iparent] threshold = 0.1 / np.abs(self.Coupl[ichild, iparent]) - Xdiff_syn_tuple *= ( - self.hill_a(x, threshold) if v else self.hill_i(x, threshold) - ) + Xdiff_syn_tuple *= self.hill_a(x, threshold) if v else self.hill_i(x, threshold) if verbosity > 0: - Xdiff_syn_tuple_str += ( - f'{"a" if v else "i"}' - f'({self.pas[child][iv]}, {threshold:.2})' - ) + Xdiff_syn_tuple_str += f'{"a" if v else "i"}' f'({self.pas[child][iv]}, {threshold:.2})' Xdiff_syn += Xdiff_syn_tuple if verbosity > 0: Xdiff_syn_str += ('+' if ituple != 0 else '') + Xdiff_syn_tuple_str # multiply with degradation term Xdiff[ichild] = self.invTimeStep * (Xdiff_syn - Xt[ichild]) if verbosity > 0: - Xdiff_str = ( - f'{child}_{child}-{child} = ' - f'{self.invTimeStep}*({Xdiff_syn_str}-{child})' - ) + Xdiff_str = f'{child}_{child}-{child} = ' f'{self.invTimeStep}*({Xdiff_syn_str}-{child})' settings.m(0, Xdiff_str) return Xdiff @@ -564,9 +540,7 @@ def read_model(self): # read couplings via names self.Coupl = np.zeros((self.dim, self.dim)) boolContinue = True - for ( - line - ) in self.model.open(): # open(self.model.replace('/model','/couplList')): + for line in self.model.open(): # open(self.model.replace('/model','/couplList')): if line.startswith('# coupling list:'): boolContinue = False if boolContinue: @@ -598,9 +572,7 @@ def set_coupl(self, Coupl=None): for g in range(self.dim): if np.abs(self.Coupl[gp, g] > 1e-10): pas.append(names[g]) - self.boolRules[names[gp]] = ''.join( - pas[:1] + [' or ' + pa for pa in pas[1:]] - ) + self.boolRules[names[gp]] = ''.join(pas[:1] + [' or ' + pa for pa in pas[1:]]) self.Adj_signed = np.sign(Coupl) elif self.model in ['6', '7', '8', '9', '10']: self.Adj_signed = np.zeros((self.dim, self.dim)) @@ -619,14 +591,10 @@ def set_coupl(self, Coupl=None): # settings.m(0,leafnodes,availnodes) while len(availnodes) != 0: # parent - parent_idx = np.random.choice( - np.arange(0, len(leafnodes)), size=1, replace=False - ) + parent_idx = np.random.choice(np.arange(0, len(leafnodes)), size=1, replace=False) parent = leafnodes[parent_idx] # children - children_ids = np.random.choice( - np.arange(0, len(availnodes)), size=2, replace=False - ) + children_ids = np.random.choice(np.arange(0, len(availnodes)), size=2, replace=False) children = availnodes[children_ids] settings.m(0, parent, children) self.Adj_signed[children, parent] = np.ones(2) @@ -653,9 +621,7 @@ def set_coupl(self, Coupl=None): # and the variable itself, therefore its # self.maxnpar+2 in the following line nr = np.random.randint(1, self.maxnpar + 2) - j_par = np.random.choice( - np.arange(0, self.dim), size=nr, replace=False - ) + j_par = np.random.choice(np.arange(0, self.dim), size=nr, replace=False) self.Adj[i, j_par] = 1 else: self.Adj[i, i] = 1 @@ -740,9 +706,7 @@ def sim_model_backwards(self, tmax, X0): X = np.zeros((tmax, self.dim)) X[tmax - 1] = X0 for t in range(tmax - 2, -1, -1): - sol = sp.optimize.root( - self.sim_model_back_help, X[t + 1], args=(X[t + 1]), method='hybr' - ) + sol = sp.optimize.root(self.sim_model_back_help, X[t + 1], args=(X[t + 1]), method='hybr') X[t] = sol.x return X @@ -752,17 +716,12 @@ def branch_init_model1(self, tmax=100): if Xfix[0] > 0.97 or Xfix[0] < 0.03: settings.m( 0, - '... either no fixed point in [0,1]^2! \n' - + ' or fixed point is too close to bounds', + '... either no fixed point in [0,1]^2! \n' + ' or fixed point is too close to bounds', ) return None # - XbackUp = self.sim_model_backwards( - tmax=tmax / 3, X0=Xfix + np.array([0.02, -0.02]) - ) - XbackDo = self.sim_model_backwards( - tmax=tmax / 3, X0=Xfix + np.array([-0.02, -0.02]) - ) + XbackUp = self.sim_model_backwards(tmax=tmax / 3, X0=Xfix + np.array([0.02, -0.02])) + XbackDo = self.sim_model_backwards(tmax=tmax / 3, X0=Xfix + np.array([-0.02, -0.02])) # Xup = self.sim_model(tmax=tmax, X0=XbackUp[0]) Xdo = self.sim_model(tmax=tmax, X0=XbackDo[0]) @@ -772,13 +731,12 @@ def branch_init_model1(self, tmax=100): if np.min(X0mean) < 0.025 or np.max(X0mean) > 0.975: settings.m(0, '... initial point is too close to bounds') return None - # if self.show and self.verbosity > 1: - pl.figure() - pl.plot(XbackUp[:, 0], '.b', XbackUp[:, 1], '.g') - pl.plot(XbackDo[:, 0], '.b', XbackDo[:, 1], '.g') - pl.plot(Xup[:, 0], 'b', Xup[:, 1], 'g') - pl.plot(Xdo[:, 0], 'b', Xdo[:, 1], 'g') + pl.figure() # noqa: F821 + pl.plot(XbackUp[:, 0], '.b', XbackUp[:, 1], '.g') # noqa: F821 + pl.plot(XbackDo[:, 0], '.b', XbackDo[:, 1], '.g') # noqa: F821 + pl.plot(Xup[:, 0], 'b', Xup[:, 1], 'g') # noqa: F821 + pl.plot(Xdo[:, 0], 'b', Xdo[:, 1], 'g') # noqa: F821 return X0mean def parents_from_boolRule(self, rule): @@ -786,13 +744,7 @@ def parents_from_boolRule(self, rule): Returns list of parents. """ - rule_pa = ( - rule.replace('(', '') - .replace(')', '') - .replace('or', '') - .replace('and', '') - .replace('not', '') - ) + rule_pa = rule.replace('(', '').replace(')', '').replace('or', '').replace('and', '').replace('not', '') rule_pa = rule_pa.split() # if there are no parents, continue if not rule_pa: @@ -839,17 +791,13 @@ def build_boolCoeff(self): raise ValueError(f'specify coupling value for {key} <- {g}') else: if np.abs(self.Coupl[self.varNames[key], g]) > 1e-10: - raise ValueError( - 'there should be no coupling value for ' f'{key} <- {g}' - ) + raise ValueError('there should be no coupling value for ' f'{key} <- {g}') if self.verbosity > 1: settings.m(0, '...' + key) settings.m(0, rule) - settings.m(0, rule_pa) + settings.m(0, rule_pa) # noqa: F821 # now evaluate coefficients - for tuple in list( - itertools.product([False, True], repeat=len(self.pas[key])) - ): + for tuple in list(itertools.product([False, True], repeat=len(self.pas[key]))): if self.process_rule(rule, self.pas[key], tuple): self.boolCoeff[key].append(tuple) # @@ -977,9 +925,7 @@ def check_nocycles(Adj: np.ndarray, verbosity: int = 2) -> bool: return True -def sample_coupling_matrix( - dim: int = 3, connectivity: float = 0.5 -) -> Tuple[np.ndarray, np.ndarray, np.ndarray, int]: +def sample_coupling_matrix(dim: int = 3, connectivity: float = 0.5) -> Tuple[np.ndarray, np.ndarray, np.ndarray, int]: """\ Sample coupling matrix. @@ -1029,9 +975,7 @@ def sample_coupling_matrix( check = True break if not check: - raise ValueError( - 'did not find graph without cycles after' f'{max_trial} trials' - ) + raise ValueError('did not find graph without cycles after' f'{max_trial} trials') return Coupl, Adj, Adj_signed, n_edges @@ -1077,16 +1021,6 @@ def sim_givenAdj(self, Adj: np.ndarray, model='line'): ------- Data array of shape (n_samples,dim). """ - # nice examples - examples = [ - dict( - func='sawtooth', - gdist='uniform', - sigma_glob=1.8, - sigma_noise=0.1, - ) - ] - # nr of samples n_samples = 100 @@ -1174,11 +1108,11 @@ def sim_combi(self): # AND type / horizontal X[:, 2] = func(X[:, 0]) * sp.stats.norm.cdf(X[:, 1], 1, 0.2) - pl.scatter(X[:, 0], X[:, 1], c=X[:, 2], edgecolor='face') - pl.show() + pl.scatter(X[:, 0], X[:, 1], c=X[:, 2], edgecolor='face') # noqa: F821 + pl.show() # noqa: F821 - pl.plot(X[:, 1], X[:, 2], '.') - pl.show() + pl.plot(X[:, 1], X[:, 2], '.') # noqa: F821 + pl.show() # noqa: F821 return X @@ -1233,8 +1167,7 @@ def sample_static_data(model, dir, verbosity=0): # command line options p = argparse.ArgumentParser( description=( - 'Simulate stochastic discrete-time dynamical systems,\n' - 'in particular gene regulatory networks.' + 'Simulate stochastic discrete-time dynamical systems,\n' 'in particular gene regulatory networks.' ), formatter_class=argparse.RawDescriptionHelpFormatter, epilog=( @@ -1273,10 +1206,7 @@ def sample_static_data(model, dir, verbosity=0): model = dir.name.split('_')[0] settings.m(0, f'...model is: {model!r}') if dir.is_dir() and 'test' not in str(dir): - message = ( - f'directory {dir} already exists, ' - 'remove it and continue? [y/n, press enter]' - ) + message = f'directory {dir} already exists, ' 'remove it and continue? [y/n, press enter]' if str(input(message)) != 'y': settings.m(0, ' ...quit program execution') sys.exit() diff --git a/scanpy/tools/_top_genes.py b/scanpy/tools/_top_genes.py index 439181b081..855fef2222 100644 --- a/scanpy/tools/_top_genes.py +++ b/scanpy/tools/_top_genes.py @@ -68,8 +68,8 @@ def correlation_matrix( # TODO: At the moment, only works for int identifiers - ### If no genes are passed, selects ranked genes from sample annotation. - ### At the moment, only calculate one table (Think about what comes next) + # If no genes are passed, selects ranked genes from sample annotation. + # At the moment, only calculate one table (Think about what comes next) if name_list is None: name_list = list() for j, k in enumerate(adata.uns['rank_genes_groups_gene_names']): @@ -128,7 +128,7 @@ def ROC_AUC_analysis( Calculate correlation matrix. Calculate a correlation matrix for genes strored in sample annotation - + Parameters ---------- adata @@ -159,7 +159,6 @@ def ROC_AUC_analysis( groups_order, groups_masks = select_groups(adata, groups, groupby) # Use usual convention, better for looping later. - imask = group mask = groups_masks[group] # TODO: Allow for sample weighting requires better mask access... later @@ -181,9 +180,7 @@ def ROC_AUC_analysis( fpr[name_list[i]], tpr[name_list[i]], thresholds[name_list[i]], - ) = metrics.roc_curve( - y_true, y_score, pos_label=None, sample_weight=None, drop_intermediate=False - ) + ) = metrics.roc_curve(y_true, y_score, pos_label=None, sample_weight=None, drop_intermediate=False) roc_auc[name_list[i]] = metrics.auc(fpr[name_list[i]], tpr[name_list[i]]) adata.uns['ROCfpr' + groupby + str(group)] = fpr adata.uns['ROCtpr' + groupby + str(group)] = tpr @@ -192,11 +189,11 @@ def ROC_AUC_analysis( def subsampled_estimates(mask, mask_rest=None, precision=0.01, probability=0.99): - ## Simple method that can be called by rank_gene_group. It uses masks that have been passed to the function and - ## calculates how much has to be subsampled in order to reach a certain precision with a certain probability - ## Then it subsamples for mask, mask rest - ## Since convergence speed varies, we take the slower one, i.e. the variance. This might have future speed-up - ## potential + # Simple method that can be called by rank_gene_group. It uses masks that have been passed to the function and + # calculates how much has to be subsampled in order to reach a certain precision with a certain probability + # Then it subsamples for mask, mask rest + # Since convergence speed varies, we take the slower one, i.e. the variance. This might have future speed-up + # potential if mask_rest is None: mask_rest = ~mask # TODO: DO precision calculation for mean variance shared @@ -205,16 +202,16 @@ def subsampled_estimates(mask, mask_rest=None, precision=0.01, probability=0.99) def dominated_ROC_elimination(adata, grouby): - ## This tool has the purpose to take a set of genes (possibly already pre-selected) and analyze AUC. - ## Those and only those are eliminated who are dominated completely - ## TODO: Potentially (But not till tomorrow), this can be adapted to only consider the AUC in the given - ## TODO: optimization frame + # This tool has the purpose to take a set of genes (possibly already pre-selected) and analyze AUC. + # Those and only those are eliminated who are dominated completely + # TODO: Potentially (But not till tomorrow), this can be adapted to only consider the AUC in the given + # TODO: optimization frame pass def _gene_preselection(adata, mask, thresholds): - ## This tool serves to - ## It is not thought to be addressed directly but rather using rank_genes_group or ROC analysis or comparable - ## TODO: Pass back a truncated adata object with only those genes that fullfill thresholding criterias - ## This function should be accessible by both rank_genes_groups and ROC_curve analysis + # This tool serves to + # It is not thought to be addressed directly but rather using rank_genes_group or ROC analysis or comparable + # TODO: Pass back a truncated adata object with only those genes that fullfill thresholding criterias + # This function should be accessible by both rank_genes_groups and ROC_curve analysis pass diff --git a/scanpy/tools/_tsne_fix.py b/scanpy/tools/_tsne_fix.py index d5a7b663b5..8ee49cd98c 100644 --- a/scanpy/tools/_tsne_fix.py +++ b/scanpy/tools/_tsne_fix.py @@ -125,16 +125,12 @@ def _gradient_descent( if verbose >= 2: print( "[t-SNE] Iteration %d: did not make any progress " - "during the last %d episodes. Finished." - % (i + 1, n_iter_without_progress) + "during the last %d episodes. Finished." % (i + 1, n_iter_without_progress) ) break if grad_norm <= min_grad_norm: if verbose >= 2: - print( - "[t-SNE] Iteration %d: gradient norm %f. Finished." - % (i + 1, grad_norm) - ) + print("[t-SNE] Iteration %d: gradient norm %f. Finished." % (i + 1, grad_norm)) break if error_diff <= min_error_diff: if verbose >= 2: diff --git a/scanpy/tools/_umap.py b/scanpy/tools/_umap.py index 28ee17c229..4267fc479e 100644 --- a/scanpy/tools/_umap.py +++ b/scanpy/tools/_umap.py @@ -122,17 +122,13 @@ def umap( neighbors_key = 'neighbors' if neighbors_key not in adata.uns: - raise ValueError( - f'Did not find .uns["{neighbors_key}"]. Run `sc.pp.neighbors` first.' - ) + raise ValueError(f'Did not find .uns["{neighbors_key}"]. Run `sc.pp.neighbors` first.') start = logg.info('computing UMAP') neighbors = NeighborsView(adata, neighbors_key) if 'params' not in neighbors or neighbors['params']['method'] != 'umap': - logg.warning( - f'.obsp["{neighbors["connectivities_key"]}"] have not been computed using umap' - ) + logg.warning(f'.obsp["{neighbors["connectivities_key"]}"] have not been computed using umap') # Compat for umap 0.4 -> 0.5 with warnings.catch_warnings(): @@ -167,9 +163,7 @@ def simplicial_set_embedding(*args, **kwargs): if isinstance(init_pos, str) and init_pos in adata.obsm.keys(): init_coords = adata.obsm[init_pos] elif isinstance(init_pos, str) and init_pos == 'paga': - init_coords = get_init_pos_from_paga( - adata, random_state=random_state, neighbors_key=neighbors_key - ) + init_coords = get_init_pos_from_paga(adata, random_state=random_state, neighbors_key=neighbors_key) else: init_coords = init_pos # Let umap handle it if hasattr(init_coords, "dtype"): @@ -216,9 +210,7 @@ def simplicial_set_embedding(*args, **kwargs): from cuml import UMAP n_neighbors = neighbors['params']['n_neighbors'] - n_epochs = ( - 500 if maxiter is None else maxiter - ) # 0 is not a valid value for rapids, unlike original umap + n_epochs = 500 if maxiter is None else maxiter # 0 is not a valid value for rapids, unlike original umap X_contiguous = np.ascontiguousarray(X, dtype=np.float32) umap = UMAP( n_neighbors=n_neighbors, diff --git a/scanpy/tools/_utils.py b/scanpy/tools/_utils.py index 856a3f1b45..3d257fe1d2 100644 --- a/scanpy/tools/_utils.py +++ b/scanpy/tools/_utils.py @@ -30,9 +30,7 @@ def _choose_representation(adata, use_rep=None, n_pcs=None, silent=False): if adata.n_vars > settings.N_PCS: if 'X_pca' in adata.obsm.keys(): if n_pcs is not None and n_pcs > adata.obsm['X_pca'].shape[1]: - raise ValueError( - '`X_pca` does not have enough PCs. Rerun `sc.pp.pca` with adjusted `n_comps`.' - ) + raise ValueError('`X_pca` does not have enough PCs. Rerun `sc.pp.pca` with adjusted `n_comps`.') X = adata.obsm['X_pca'][:, :n_pcs] logg.info(f' using \'X_pca\' with n_pcs = {X.shape[1]}') else: @@ -54,10 +52,7 @@ def _choose_representation(adata, use_rep=None, n_pcs=None, silent=False): elif use_rep == 'X': X = adata.X else: - raise ValueError( - 'Did not find {} in `.obsm.keys()`. ' - 'You need to compute it first.'.format(use_rep) - ) + raise ValueError('Did not find {} in `.obsm.keys()`. ' 'You need to compute it first.'.format(use_rep)) settings.verbosity = verbosity # resetting verbosity return X @@ -93,9 +88,7 @@ def preprocess_with_pca(adata, n_pcs: Optional[int] = None, random_state=0): return adata.X -def get_init_pos_from_paga( - adata, adjacency=None, random_state=0, neighbors_key=None, obsp=None -): +def get_init_pos_from_paga(adata, adjacency=None, random_state=0, neighbors_key=None, obsp=None): np.random.seed(random_state) if adjacency is None: adjacency = _choose_graph(adata, obsp, neighbors_key) @@ -117,7 +110,5 @@ def get_init_pos_from_paga( else: init_pos[subset] = group_pos else: - raise ValueError( - 'Plot PAGA first, so that adata.uns[\'paga\']' 'with key \'pos\'.' - ) + raise ValueError('Plot PAGA first, so that adata.uns[\'paga\']' 'with key \'pos\'.') return init_pos diff --git a/scanpy/tools/_utils_clustering.py b/scanpy/tools/_utils_clustering.py index f7b331e6ca..b46b9a2387 100644 --- a/scanpy/tools/_utils_clustering.py +++ b/scanpy/tools/_utils_clustering.py @@ -1,6 +1,4 @@ -def rename_groups( - adata, key_added, restrict_key, restrict_categories, restrict_indices, groups -): +def rename_groups(adata, key_added, restrict_key, restrict_categories, restrict_indices, groups): key_added = restrict_key + '_R' if key_added is None else key_added all_groups = adata.obs[restrict_key].astype('U') prefix = '-'.join(restrict_categories) + ',' @@ -11,14 +9,10 @@ def rename_groups( def restrict_adjacency(adata, restrict_key, restrict_categories, adjacency): if not isinstance(restrict_categories[0], str): - raise ValueError( - 'You need to use strings to label categories, ' 'e.g. \'1\' instead of 1.' - ) + raise ValueError('You need to use strings to label categories, ' 'e.g. \'1\' instead of 1.') for c in restrict_categories: if c not in adata.obs[restrict_key].cat.categories: - raise ValueError( - '\'{}\' is not a valid category for \'{}\''.format(c, restrict_key) - ) + raise ValueError('\'{}\' is not a valid category for \'{}\''.format(c, restrict_key)) restrict_indices = adata.obs[restrict_key].isin(restrict_categories).values adjacency = adjacency[restrict_indices, :] adjacency = adjacency[:, restrict_indices] diff --git a/setup.py b/setup.py index b75b02f38a..f0a8a941f6 100644 --- a/setup.py +++ b/setup.py @@ -25,9 +25,7 @@ author_email=metadata['author-email'], license='BSD', python_requires='>=3.6', - install_requires=[ - l.strip() for l in Path('requirements.txt').read_text('utf-8').splitlines() - ], + install_requires=[line.strip() for line in Path('requirements.txt').read_text('utf-8').splitlines()], extras_require=dict( louvain=['python-igraph', 'louvain>=0.6,!=0.6.2'], leiden=['python-igraph', 'leidenalg'], From 55737d99d47d7bf9f1a5644c4032a96c95ee888d Mon Sep 17 00:00:00 2001 From: Zethson Date: Wed, 24 Feb 2021 13:27:31 +0100 Subject: [PATCH 02/85] fix pre-commit Signed-off-by: Zethson --- scanpy/plotting/_baseplot_class.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scanpy/plotting/_baseplot_class.py b/scanpy/plotting/_baseplot_class.py index ada2330436..a8872b2c32 100644 --- a/scanpy/plotting/_baseplot_class.py +++ b/scanpy/plotting/_baseplot_class.py @@ -118,9 +118,9 @@ def __init__( "the `order` parameter match the categories that " "want to be reordered.\n\n" "Mismatch: " - f"{set(obs_tidy.index.categories).difference(categories_order)}\n\n" + f"{set(obs_tidy.index.categories).difference(categories_order)}\n\n" # noqa: F821 f"Given order categories: {categories_order}\n\n" - f"{groupby} categories: {list(obs_tidy.index.categories)}\n" + f"{groupby} categories: {list(obs_tidy.index.categories)}\n" # noqa: F821 ) return From f653e5ab4c4b97ad19661eef897d0a65874fe01b Mon Sep 17 00:00:00 2001 From: Zethson Date: Wed, 24 Feb 2021 13:46:29 +0100 Subject: [PATCH 03/85] add E402 to flake8 ignore Signed-off-by: Zethson --- .flake8 | 2 +- scanpy/__init__.py | 44 +++++++++---------- scanpy/tests/conftest.py | 9 ++-- .../notebooks/test_paga_paul15_subsampled.py | 3 +- scanpy/tests/notebooks/test_pbmc3k.py | 3 +- scanpy/tests/test_plotting.py | 18 ++++---- 6 files changed, 42 insertions(+), 37 deletions(-) diff --git a/.flake8 b/.flake8 index ac3ef0697a..6787079d5c 100644 --- a/.flake8 +++ b/.flake8 @@ -1,4 +1,4 @@ [flake8] exclude = docs, scanpy/tests max-line-length = 120 -ignore = F401, W503, E501, E203, E231, W504 +ignore = F401, W503, E501, E203, E231, W504, E402 diff --git a/scanpy/__init__.py b/scanpy/__init__.py index 4f2f5ef1af..71eb75e977 100644 --- a/scanpy/__init__.py +++ b/scanpy/__init__.py @@ -1,9 +1,23 @@ """Single-Cell Analysis in Python.""" -from ._utils import annotate_doc_types -import sys -from .neighbors import Neighbors -from .readwrite import read, read_10x_h5, read_10x_mtx, write, read_visium +from ._metadata import __version__, __author__, __email__ + +from ._utils import check_versions + +check_versions() +del check_versions + +# the actual API +from ._settings import ( + settings, + Verbosity, +) # start with settings as several tools are using it +from . import tools as tl +from . import preprocessing as pp +from . import plotting as pl +from . import datasets, logging, queries, external, get + +from anndata import AnnData, concat from anndata import ( read_h5ad, read_csv, @@ -14,30 +28,16 @@ read_text, read_umi_tools, ) -from anndata import AnnData, concat -from . import datasets, logging, queries, external, get -from . import plotting as pl -from . import preprocessing as pp -from . import tools as tl -from ._settings import ( - settings, - Verbosity, -) # start with settings as several tools are using it -from ._metadata import __version__, __author__, __email__ - -from ._utils import check_versions - -check_versions() -del check_versions - -# the actual API - +from .readwrite import read, read_10x_h5, read_10x_mtx, write, read_visium +from .neighbors import Neighbors set_figure_params = settings.set_figure_params # has to be done at the end, after everything has been imported +import sys sys.modules.update({f'{__name__}.{m}': globals()[m] for m in ['tl', 'pp', 'pl']}) +from ._utils import annotate_doc_types annotate_doc_types(sys.modules[__name__], 'scanpy') del sys, annotate_doc_types diff --git a/scanpy/tests/conftest.py b/scanpy/tests/conftest.py index 163cabeba0..574dbf3543 100644 --- a/scanpy/tests/conftest.py +++ b/scanpy/tests/conftest.py @@ -1,13 +1,14 @@ -import scanpy -import pytest -from matplotlib.testing.compare import compare_images, make_test_filename -from matplotlib import pyplot import sys from pathlib import Path import matplotlib as mpl mpl.use('agg') +from matplotlib import pyplot +from matplotlib.testing.compare import compare_images, make_test_filename +import pytest + +import scanpy scanpy.settings.verbosity = "hint" diff --git a/scanpy/tests/notebooks/test_paga_paul15_subsampled.py b/scanpy/tests/notebooks/test_paga_paul15_subsampled.py index 38c3b4f7d3..cbf7c0d252 100644 --- a/scanpy/tests/notebooks/test_paga_paul15_subsampled.py +++ b/scanpy/tests/notebooks/test_paga_paul15_subsampled.py @@ -3,7 +3,6 @@ # # This is the subsampled notebook for testing. -import scanpy as sc from pathlib import Path import numpy as np @@ -11,6 +10,8 @@ setup() +import scanpy as sc + HERE: Path = Path(__file__).parent ROOT = HERE / '_images_paga_paul15_subsampled' diff --git a/scanpy/tests/notebooks/test_pbmc3k.py b/scanpy/tests/notebooks/test_pbmc3k.py index 42bd171986..b7198d972b 100644 --- a/scanpy/tests/notebooks/test_pbmc3k.py +++ b/scanpy/tests/notebooks/test_pbmc3k.py @@ -10,7 +10,6 @@ # ([here](http://cf.10xgenomics.com/samples/cell-exp/1.1.0/pbmc3k/pbmc3k_filtered_gene_bc_matrices.tar.gz) # from this [webpage](https://support.10xgenomics.com/single-cell-gene-expression/datasets/1.1.0/pbmc3k)). -import scanpy as sc from pathlib import Path import numpy as np @@ -20,6 +19,8 @@ setup() +import scanpy as sc + HERE: Path = Path(__file__).parent ROOT = HERE / 'pbmc3k_images' diff --git a/scanpy/tests/test_plotting.py b/scanpy/tests/test_plotting.py index 69a15a5494..e04c943a94 100644 --- a/scanpy/tests/test_plotting.py +++ b/scanpy/tests/test_plotting.py @@ -1,11 +1,3 @@ -import scanpy as sc -from anndata import AnnData -from matplotlib.testing.compare import compare_images -import pandas as pd -import numpy as np -import matplotlib.cm as cm -import matplotlib.pyplot as plt -import matplotlib as mpl from functools import partial from pathlib import Path from itertools import repeat, chain, combinations @@ -18,6 +10,16 @@ setup() +import matplotlib as mpl +import matplotlib.pyplot as plt +import matplotlib.cm as cm +import numpy as np +import pandas as pd +from matplotlib.testing.compare import compare_images +from anndata import AnnData + +import scanpy as sc + HERE: Path = Path(__file__).parent ROOT = HERE / '_images' From daf03c9810df8fade1f3598f86cb9579b5a8d770 Mon Sep 17 00:00:00 2001 From: Zethson Date: Wed, 24 Feb 2021 14:07:13 +0100 Subject: [PATCH 04/85] revert neighbors Signed-off-by: Zethson --- .flake8 | 2 +- scanpy/neighbors/__init__.py | 25 +++++++++++++++---------- scanpy/tests/test_plotting.py | 1 - 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/.flake8 b/.flake8 index 6787079d5c..b50e5e7d55 100644 --- a/.flake8 +++ b/.flake8 @@ -1,4 +1,4 @@ [flake8] exclude = docs, scanpy/tests max-line-length = 120 -ignore = F401, W503, E501, E203, E231, W504, E402 +ignore = F401, W503, E501, E203, E231, W504, E402, E126, E712, E741 diff --git a/scanpy/neighbors/__init__.py b/scanpy/neighbors/__init__.py index 26152bbfb5..cd30693f7d 100644 --- a/scanpy/neighbors/__init__.py +++ b/scanpy/neighbors/__init__.py @@ -120,6 +120,12 @@ def neighbors( **distances** : sparse matrix of dtype `float32`. Instead of decaying weights, this stores distances for each pair of neighbors. + + Notes + ----- + If `method='umap'`, it's highly recommended to install pynndescent ``pip install pynndescent``. + Installing `pynndescent` can significantly increase performance, + and in later versions it will become a hard dependency. """ start = logg.info('computing neighbors') adata = adata.copy() if copy else adata @@ -739,7 +745,7 @@ def compute_neighbors( self.knn = knn X = _choose_representation(self._adata, use_rep=use_rep, n_pcs=n_pcs) # neighbor search - use_dense_distances = (metric == 'euclidean' and X.shape[0] < 8192) or not knn + use_dense_distances = (metric == 'euclidean' and X.shape[0] < 8192) or knn == False if use_dense_distances: _distances = pairwise_distances(X, metric=metric, **metric_kwds) knn_indices, knn_distances = _get_indices_distances_from_dense_matrix(_distances, n_neighbors) @@ -824,7 +830,7 @@ def _compute_connectivities_diffmap(self, density_normalize=True): # make the weight matrix sparse if not self.knn: mask = W > 1e-14 - W[not mask] = 0 + W[mask == False] = 0 else: # restrict number of neighbors to ~k # build a symmetric mask @@ -836,7 +842,7 @@ def _compute_connectivities_diffmap(self, density_normalize=True): W[j, i] = W[i, j] mask[j, i] = True # set all entries that are not nearest neighbors to zero - W[not mask] = 0 + W[mask == False] = 0 else: W = Dsq.copy() # need to copy the distance matrix here; what follows is inplace for i in range(len(Dsq.indptr[:-1])): @@ -979,11 +985,10 @@ def _get_dpt_row(self, i): label = self._connected_components[1][i] mask = self._connected_components[1] == label row = sum( - (self.eigen_values[i] / (1 - self.eigen_values[i]) * (self.eigen_basis[i, i] - self.eigen_basis[:, i])) - ** 2 # noqa: E126 + (self.eigen_values[l] / (1 - self.eigen_values[l]) * (self.eigen_basis[i, l] - self.eigen_basis[:, l])) ** 2 # account for float32 precision - for i in range(0, self.eigen_values.size) - if self.eigen_values[i] < 0.9994 + for l in range(0, self.eigen_values.size) + if self.eigen_values[l] < 0.9994 ) # thanks to Marius Lange for pointing Alex to this: # we will likely remove the contributions from the stationary state below when making @@ -991,9 +996,9 @@ def _get_dpt_row(self, i): # they never seem to have deteriorated results, but also other distance measures (see e.g. # PAGA paper) don't have it, which makes sense row += sum( - (self.eigen_basis[i, j] - self.eigen_basis[:, j]) ** 2 - for j in range(0, self.eigen_values.size) - if self.eigen_values[j] >= 0.9994 + (self.eigen_basis[i, l] - self.eigen_basis[:, l]) ** 2 + for l in range(0, self.eigen_values.size) + if self.eigen_values[l] >= 0.9994 ) if mask is not None: row[~mask] = np.inf diff --git a/scanpy/tests/test_plotting.py b/scanpy/tests/test_plotting.py index e04c943a94..265c7b8220 100644 --- a/scanpy/tests/test_plotting.py +++ b/scanpy/tests/test_plotting.py @@ -20,7 +20,6 @@ import scanpy as sc - HERE: Path = Path(__file__).parent ROOT = HERE / '_images' FIGS = HERE / 'figures' From 2b79a88b7cf3a8553606c59d2d9c70b4c329cf2d Mon Sep 17 00:00:00 2001 From: Zethson Date: Wed, 24 Feb 2021 14:21:50 +0100 Subject: [PATCH 05/85] fix flake8 Signed-off-by: Zethson --- scanpy/preprocessing/_normalization.py | 2 +- scanpy/tools/_diffmap.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/scanpy/preprocessing/_normalization.py b/scanpy/preprocessing/_normalization.py index 41b7a77d49..5bb4f7deb6 100644 --- a/scanpy/preprocessing/_normalization.py +++ b/scanpy/preprocessing/_normalization.py @@ -38,7 +38,7 @@ def normalize_total( """\ Normalize counts per cell. - Normalize each cell by total counts over all genes, + Normalize each cell by total counts over all genes, so that every cell has the same total count after normalization. If choosing `target_sum=1e6`, this is CPM normalization. diff --git a/scanpy/tools/_diffmap.py b/scanpy/tools/_diffmap.py index b4f5c11059..a04e3b4a1a 100644 --- a/scanpy/tools/_diffmap.py +++ b/scanpy/tools/_diffmap.py @@ -52,10 +52,10 @@ def diffmap( `diffmap_evals` : :class:`numpy.ndarray` (`adata.uns`) Array of size (number of eigen vectors). Eigenvalues of transition matrix. - + Notes ----- - The 0-th column in `adata.obsm["X_diffmap"]` is the steady-state solution, + The 0-th column in `adata.obsm["X_diffmap"]` is the steady-state solution, which is non-informative in diffusion maps. Therefore, the first diffusion component is at index 1, e.g. `adata.obsm["X_diffmap"][:,1]` From 617168f7a7ebdf31d9a0fe1b6dbd375a587a7011 Mon Sep 17 00:00:00 2001 From: Zethson Date: Thu, 25 Feb 2021 10:44:48 +0100 Subject: [PATCH 06/85] address review Signed-off-by: Zethson --- .flake8 | 5 +- docs/extensions/function_images.py | 4 +- docs/extensions/github_links.py | 4 +- pyproject.toml | 2 +- scanpy/_settings.py | 25 ++- scanpy/_utils.py | 78 +++++-- scanpy/cli.py | 24 ++- scanpy/datasets/_datasets.py | 20 +- scanpy/datasets/_ebi_expression_atlas.py | 4 +- scanpy/external/exporting.py | 74 +++++-- scanpy/external/pl.py | 8 +- scanpy/external/pp/_hashsolo.py | 134 ++++++++---- scanpy/external/pp/_magic.py | 11 +- scanpy/external/pp/_mnn_correct.py | 11 +- scanpy/external/pp/_scanorama_integrate.py | 4 +- scanpy/external/pp/_scrublet.py | 28 ++- scanpy/external/pp/_scvi.py | 12 +- scanpy/external/tl/_palantir.py | 4 +- scanpy/external/tl/_phate.py | 3 +- scanpy/external/tl/_phenograph.py | 11 +- scanpy/external/tl/_trimap.py | 3 +- scanpy/external/tl/_wishbone.py | 22 +- scanpy/get/get.py | 21 +- scanpy/logging.py | 12 +- scanpy/neighbors/__init__.py | 86 ++++++-- scanpy/plotting/_anndata.py | 152 +++++++++---- scanpy/plotting/_baseplot_class.py | 44 +++- scanpy/plotting/_dotplot.py | 35 ++- scanpy/plotting/_matrixplot.py | 4 +- scanpy/plotting/_preprocessing.py | 4 +- scanpy/plotting/_qc.py | 10 +- scanpy/plotting/_stacked_violin.py | 38 +++- scanpy/plotting/_tools/__init__.py | 44 +++- scanpy/plotting/_tools/paga.py | 138 +++++++++--- scanpy/plotting/_tools/scatterplots.py | 137 +++++++++--- scanpy/plotting/_utils.py | 70 ++++-- scanpy/plotting/palettes.py | 4 +- scanpy/preprocessing/_combat.py | 33 ++- scanpy/preprocessing/_deprecated/__init__.py | 10 +- .../_deprecated/highly_variable_genes.py | 22 +- .../preprocessing/_highly_variable_genes.py | 62 ++++-- scanpy/preprocessing/_normalization.py | 8 +- scanpy/preprocessing/_pca.py | 16 +- scanpy/preprocessing/_qc.py | 51 +++-- scanpy/preprocessing/_recipes.py | 16 +- scanpy/preprocessing/_simple.py | 78 +++++-- scanpy/preprocessing/_utils.py | 4 +- scanpy/queries/_queries.py | 15 +- scanpy/readwrite.py | 65 ++++-- scanpy/tests/external/test_hashsolo.py | 4 +- scanpy/tests/external/test_scrublet.py | 4 +- scanpy/tests/external/test_wishbone.py | 4 +- scanpy/tests/helpers.py | 4 +- .../notebooks/test_paga_paul15_subsampled.py | 4 +- scanpy/tests/notebooks/test_pbmc3k.py | 12 +- scanpy/tests/test_combat.py | 4 +- scanpy/tests/test_datasets.py | 13 +- scanpy/tests/test_docs.py | 4 +- scanpy/tests/test_embedding_plots.py | 48 +++-- scanpy/tests/test_filter_rank_genes_groups.py | 4 +- scanpy/tests/test_get.py | 20 +- scanpy/tests/test_highly_variable_genes.py | 40 +++- scanpy/tests/test_ingest.py | 4 +- scanpy/tests/test_neighbors.py | 8 +- scanpy/tests/test_neighbors_key_added.py | 8 +- scanpy/tests/test_pca.py | 15 +- scanpy/tests/test_plotting.py | 48 +++-- scanpy/tests/test_preprocessing.py | 45 +++- .../tests/test_preprocessing_distributed.py | 10 +- scanpy/tests/test_qc_metrics.py | 37 +++- scanpy/tests/test_queries.py | 8 +- scanpy/tests/test_rank_genes_groups.py | 50 +++-- scanpy/tests/test_rank_genes_groups_logreg.py | 4 +- scanpy/tests/test_read_10x.py | 8 +- scanpy/tests/test_score_genes.py | 23 +- scanpy/tools/_dendrogram.py | 12 +- scanpy/tools/_diffmap.py | 4 +- scanpy/tools/_dpt.py | 204 ++++++++++++++---- scanpy/tools/_draw_graph.py | 4 +- scanpy/tools/_embedding_density.py | 7 +- scanpy/tools/_ingest.py | 48 +++-- scanpy/tools/_louvain.py | 11 +- scanpy/tools/_marker_gene_overlap.py | 52 +++-- scanpy/tools/_paga.py | 84 ++++++-- scanpy/tools/_rank_genes_groups.py | 68 ++++-- scanpy/tools/_score_genes.py | 20 +- scanpy/tools/_sim.py | 107 ++++++--- scanpy/tools/_top_genes.py | 4 +- scanpy/tools/_tsne_fix.py | 8 +- scanpy/tools/_umap.py | 16 +- scanpy/tools/_utils.py | 17 +- scanpy/tools/_utils_clustering.py | 12 +- setup.py | 5 +- 93 files changed, 2103 insertions(+), 683 deletions(-) diff --git a/.flake8 b/.flake8 index b50e5e7d55..1a0e05ba84 100644 --- a/.flake8 +++ b/.flake8 @@ -1,4 +1,5 @@ +# Can't yet be moved to the pyproject.toml due to https://gitlab.com/pycqa/flake8/-/issues/428#note_251982786 [flake8] -exclude = docs, scanpy/tests -max-line-length = 120 +max-line-length = 88 +// switched off since they conflict with black's standards ignore = F401, W503, E501, E203, E231, W504, E402, E126, E712, E741 diff --git a/docs/extensions/function_images.py b/docs/extensions/function_images.py index f2a72840f1..42aac73c26 100644 --- a/docs/extensions/function_images.py +++ b/docs/extensions/function_images.py @@ -6,7 +6,9 @@ from sphinx.ext.autodoc import Options -def insert_function_images(app: Sphinx, what: str, name: str, obj: Any, options: Options, lines: List[str]): +def insert_function_images( + app: Sphinx, what: str, name: str, obj: Any, options: Options, lines: List[str] +): path = app.config.api_dir / f'{name}.png' if what != 'function' or not path.is_file(): return diff --git a/docs/extensions/github_links.py b/docs/extensions/github_links.py index f01106fc4b..a2863627c0 100644 --- a/docs/extensions/github_links.py +++ b/docs/extensions/github_links.py @@ -32,7 +32,9 @@ def __call__( def register_links(app: Sphinx, config: Config): - gh_url = 'https://github.com/{github_user}/{github_repo}'.format_map(config.html_context) + gh_url = 'https://github.com/{github_user}/{github_repo}'.format_map( + config.html_context + ) app.add_role('pr', AutoLink('pr', f'{gh_url}/pull/{{}}', 'PR {}')) app.add_role('issue', AutoLink('issue', f'{gh_url}/issues/{{}}', 'issue {}')) app.add_role('noteversion', AutoLink('noteversion', f'{gh_url}/releases/tag/{{}}')) diff --git a/pyproject.toml b/pyproject.toml index dc5963a982..e1d37256b8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,7 @@ source = ['scanpy'] omit = ['*/tests/*'] [tool.black] -line-length = 120 +line-length = 88 target-version = ['py38'] skip-string-normalization = true exclude = ''' diff --git a/scanpy/_settings.py b/scanpy/_settings.py index fdb4ba3c10..e217b36083 100644 --- a/scanpy/_settings.py +++ b/scanpy/_settings.py @@ -53,7 +53,9 @@ def _type_check(var: Any, varname: str, types: Union[type, Tuple[type, ...]]): possible_types_str = types.__name__ else: type_names = [t.__name__ for t in types] - possible_types_str = "{} or {}".format(", ".join(type_names[:-1]), type_names[-1]) + possible_types_str = "{} or {}".format( + ", ".join(type_names[:-1]), type_names[-1] + ) raise TypeError(f"{varname} must be of type {possible_types_str}") @@ -139,7 +141,9 @@ def verbosity(self) -> Verbosity: @verbosity.setter def verbosity(self, verbosity: Union[Verbosity, int, str]): - verbosity_str_options = [v for v in _VERBOSITY_TO_LOGLEVEL if isinstance(v, str)] + verbosity_str_options = [ + v for v in _VERBOSITY_TO_LOGLEVEL if isinstance(v, str) + ] if isinstance(verbosity, Verbosity): self._verbosity = verbosity elif isinstance(verbosity, int): @@ -148,7 +152,8 @@ def verbosity(self, verbosity: Union[Verbosity, int, str]): verbosity = verbosity.lower() if verbosity not in verbosity_str_options: raise ValueError( - f"Cannot set verbosity to {verbosity}. " f"Accepted string values are: {verbosity_str_options}" + f"Cannot set verbosity to {verbosity}. " + f"Accepted string values are: {verbosity_str_options}" ) else: self._verbosity = Verbosity(verbosity_str_options.index(verbosity)) @@ -180,7 +185,10 @@ def file_format_data(self, file_format: str): _type_check(file_format, "file_format_data", str) file_format_options = {"txt", "csv", "h5ad"} if file_format not in file_format_options: - raise ValueError(f"Cannot set file_format_data to {file_format}. " f"Must be one of {file_format_options}") + raise ValueError( + f"Cannot set file_format_data to {file_format}. " + f"Must be one of {file_format_options}" + ) self._file_format_data = file_format @property @@ -285,7 +293,10 @@ def cache_compression(self) -> Optional[str]: @cache_compression.setter def cache_compression(self, cache_compression: Optional[str]): if cache_compression not in {'lzf', 'gzip', None}: - raise ValueError(f"`cache_compression` ({cache_compression}) " "must be in {'lzf', 'gzip', None}") + raise ValueError( + f"`cache_compression` ({cache_compression}) " + "must be in {'lzf', 'gzip', None}" + ) self._cache_compression = cache_compression @property @@ -464,7 +475,9 @@ def _is_run_from_ipython(): def __str__(self) -> str: return '\n'.join( - f'{k} = {v!r}' for k, v in inspect.getmembers(self) if not k.startswith("_") and not k == 'getdoc' + f'{k} = {v!r}' + for k, v in inspect.getmembers(self) + if not k.startswith("_") and not k == 'getdoc' ) diff --git a/scanpy/_utils.py b/scanpy/_utils.py index d9929dc3a0..85bd0fdf8e 100644 --- a/scanpy/_utils.py +++ b/scanpy/_utils.py @@ -54,7 +54,9 @@ def check_versions(): # make this a warning, not an error # it might be useful for people to still be able to run it - logg.warning(f'Scanpy {__version__} needs umap ' f'version >=0.3.0, not {umap_version}.') + logg.warning( + f'Scanpy {__version__} needs umap ' f'version >=0.3.0, not {umap_version}.' + ) def getdoc(c_or_f: Union[Callable, type]) -> Optional[str]: @@ -75,7 +77,8 @@ def type_doc(name: str): return cls return '\n'.join( - f'{line} : {type_doc(line)}' if line.strip() in sig.parameters else line for line in doc.split('\n') + f'{line} : {type_doc(line)}' if line.strip() in sig.parameters else line + for line in doc.split('\n') ) @@ -119,7 +122,9 @@ def _one_of_ours(obj, root: str): return ( hasattr(obj, "__name__") and not obj.__name__.split(".")[-1].startswith("_") - and getattr(obj, '__module__', getattr(obj, '__qualname__', obj.__name__)).startswith(root) + and getattr( + obj, '__module__', getattr(obj, '__qualname__', obj.__name__) + ).startswith(root) ) @@ -166,7 +171,9 @@ def _check_array_function_arguments(**kwargs): # TODO: Figure out a better solution for documenting dispatched functions invalid_args = [k for k, v in kwargs.items() if v is not None] if len(invalid_args) > 0: - raise TypeError(f"Arguments {invalid_args} are only valid if an AnnData object is passed.") + raise TypeError( + f"Arguments {invalid_args} are only valid if an AnnData object is passed." + ) def _check_use_raw(adata: AnnData, use_raw: Union[None, bool]) -> bool: @@ -206,7 +213,8 @@ def get_igraph_from_adjacency(adjacency, directed=None): pass if g.vcount() != adjacency.shape[0]: logg.warning( - f'The constructed graph has only {g.vcount()} nodes. ' 'Your adjacency matrix contained redundant nodes.' + f'The constructed graph has only {g.vcount()} nodes. ' + 'Your adjacency matrix contained redundant nodes.' ) return g @@ -273,12 +281,17 @@ def compute_association_matrix_of_groups( reference labels, entries are proportional to degree of association. """ if normalization not in {'prediction', 'reference'}: - raise ValueError('`normalization` needs to be either "prediction" or "reference".') + raise ValueError( + '`normalization` needs to be either "prediction" or "reference".' + ) sanitize_anndata(adata) cats = adata.obs[reference].cat.categories for cat in cats: if cat in settings.categories_to_ignore: - logg.info(f'Ignoring category {cat!r} ' 'as it’s in `settings.categories_to_ignore`.') + logg.info( + f'Ignoring category {cat!r} ' + 'as it’s in `settings.categories_to_ignore`.' + ) asso_names = [] asso_matrix = [] for ipred_group, pred_group in enumerate(adata.obs[prediction].cat.categories): @@ -297,11 +310,15 @@ def compute_association_matrix_of_groups( if normalization == 'prediction': # compute which fraction of the predicted group is contained in # the ref group - ratio_contained = (np.sum(mask_pred_int) - np.sum(mask_ref_or_pred - mask_ref)) / np.sum(mask_pred_int) + ratio_contained = ( + np.sum(mask_pred_int) - np.sum(mask_ref_or_pred - mask_ref) + ) / np.sum(mask_pred_int) else: # compute which fraction of the reference group is contained in # the predicted group - ratio_contained = (np.sum(mask_ref) - np.sum(mask_ref_or_pred - mask_pred_int)) / np.sum(mask_ref) + ratio_contained = ( + np.sum(mask_ref) - np.sum(mask_ref_or_pred - mask_pred_int) + ) / np.sum(mask_ref) asso_matrix[-1] += [ratio_contained] name_list_pred = [ cats[i] if cats[i] not in settings.categories_to_ignore else '' @@ -309,13 +326,18 @@ def compute_association_matrix_of_groups( if asso_matrix[-1][i] > threshold ] asso_names += ['\n'.join(name_list_pred[:max_n_names])] - Result = namedtuple('compute_association_matrix_of_groups', ['asso_names', 'asso_matrix']) + Result = namedtuple( + 'compute_association_matrix_of_groups', ['asso_names', 'asso_matrix'] + ) return Result(asso_names=asso_names, asso_matrix=np.array(asso_matrix)) def get_associated_colors_of_groups(reference_colors, asso_matrix): return [ - {reference_colors[i_ref]: asso_matrix[i_pred, i_ref] for i_ref in range(asso_matrix.shape[1])} + { + reference_colors[i_ref]: asso_matrix[i_pred, i_ref] + for i_ref in range(asso_matrix.shape[1]) + } for i_pred in range(asso_matrix.shape[0]) ] @@ -344,9 +366,16 @@ def identify_groups(ref_labels, pred_labels, return_overlaps=False): associated_predictions = {} associated_overlaps = {} for ref_label in ref_unique: - sub_pred_unique, sub_pred_counts = np.unique(pred_labels[ref_label == ref_labels], return_counts=True) - relative_overlaps_pred = [sub_pred_counts[i] / pred_dict[n] for i, n in enumerate(sub_pred_unique)] - relative_overlaps_ref = [sub_pred_counts[i] / ref_dict[ref_label] for i, n in enumerate(sub_pred_unique)] + sub_pred_unique, sub_pred_counts = np.unique( + pred_labels[ref_label == ref_labels], return_counts=True + ) + relative_overlaps_pred = [ + sub_pred_counts[i] / pred_dict[n] for i, n in enumerate(sub_pred_unique) + ] + relative_overlaps_ref = [ + sub_pred_counts[i] / ref_dict[ref_label] + for i, n in enumerate(sub_pred_unique) + ] relative_overlaps = np.c_[relative_overlaps_pred, relative_overlaps_ref] relative_overlaps_min = np.min(relative_overlaps, axis=1) pred_best_index = np.argsort(relative_overlaps_min)[::-1] @@ -470,7 +499,9 @@ def select_groups(adata, groups_order_subset='all', key='groups'): if key + '_masks' in adata.uns: groups_masks = adata.uns[key + '_masks'] else: - groups_masks = np.zeros((len(adata.obs[key].cat.categories), adata.obs[key].values.size), dtype=bool) + groups_masks = np.zeros( + (len(adata.obs[key].cat.categories), adata.obs[key].values.size), dtype=bool + ) for iname, name in enumerate(adata.obs[key].cat.categories): # if the name is not found, fallback to index retrieval if adata.obs[key].cat.categories[iname] in adata.obs[key].values: @@ -482,7 +513,9 @@ def select_groups(adata, groups_order_subset='all', key='groups'): if groups_order_subset != 'all': groups_ids = [] for name in groups_order_subset: - groups_ids.append(np.where(adata.obs[key].cat.categories.values == name)[0][0]) + groups_ids.append( + np.where(adata.obs[key].cat.categories.values == name)[0][0] + ) if len(groups_ids) == 0: # fallback to index retrieval groups_ids = np.where( @@ -564,7 +597,9 @@ def subsample( return Xsampled, rows -def subsample_n(X: np.ndarray, n: int = 0, seed: int = 0) -> Tuple[np.ndarray, np.ndarray]: +def subsample_n( + X: np.ndarray, n: int = 0, seed: int = 0 +) -> Tuple[np.ndarray, np.ndarray]: """Subsample n samples from rows of array. Parameters @@ -713,12 +748,17 @@ def __contains__(self, key): def _choose_graph(adata, obsp, neighbors_key): """Choose connectivities from neighbbors or another obsp column""" if obsp is not None and neighbors_key is not None: - raise ValueError('You can\'t specify both obsp, neighbors_key. ' 'Please select only one.') + raise ValueError( + 'You can\'t specify both obsp, neighbors_key. ' 'Please select only one.' + ) if obsp is not None: return adata.obsp[obsp] else: neighbors = NeighborsView(adata, neighbors_key) if 'connectivities' not in neighbors: - raise ValueError('You need to run `pp.neighbors` first ' 'to compute a neighborhood graph.') + raise ValueError( + 'You need to run `pp.neighbors` first ' + 'to compute a neighborhood graph.' + ) return neighbors['connectivities'] diff --git a/scanpy/cli.py b/scanpy/cli.py index b7529d52e6..9a90cebd31 100644 --- a/scanpy/cli.py +++ b/scanpy/cli.py @@ -25,7 +25,9 @@ class _DelegatingSubparsersAction(_SubParsersAction): def __init__(self, *args, _command: str, _runargs: Dict[str, Any], **kwargs): super().__init__(*args, **kwargs) self.command = _command - self._name_parser_map = self.choices = _CommandDelegator(_command, self, **_runargs) + self._name_parser_map = self.choices = _CommandDelegator( + _command, self, **_runargs + ) class _CommandDelegator(cabc.MutableMapping): @@ -47,7 +49,9 @@ def __getitem__(self, k: str) -> ArgumentParser: if which(f'{self.command}-{k}'): return _DelegatingParser(self, k) # Only here is the command list retrieved - raise ArgumentError(self.action, f'No command “{k}”. Choose from {set(self)}') + raise ArgumentError( + self.action, f'No command “{k}”. Choose from {set(self)}' + ) def __setitem__(self, k: str, v: ArgumentParser) -> None: self.parser_map[k] = v @@ -70,7 +74,8 @@ def __hash__(self) -> int: def __eq__(self, other: Mapping[str, ArgumentParser]): if isinstance(other, _CommandDelegator): return all( - getattr(self, attr) == getattr(other, attr) for attr in ['command', 'action', 'parser_map', 'runargs'] + getattr(self, attr) == getattr(other, attr) + for attr in ['command', 'action', 'parser_map', 'runargs'] ) return self.parser_map == other @@ -98,7 +103,9 @@ def parse_known_args( args: Optional[Sequence[str]] = None, namespace: Optional[Namespace] = None, ) -> Tuple[Namespace, List[str]]: - assert args is not None and namespace is None, 'Only use DelegatingParser as subparser' + assert ( + args is not None and namespace is None + ), 'Only use DelegatingParser as subparser' return Namespace(func=partial(run, [self.prog, *args], **self.cd.runargs)), [] @@ -108,7 +115,9 @@ def _cmd_settings() -> None: print(settings) -def main(argv: Optional[Sequence[str]] = None, *, check: bool = True, **runargs) -> Optional[CompletedProcess]: +def main( + argv: Optional[Sequence[str]] = None, *, check: bool = True, **runargs +) -> Optional[CompletedProcess]: """\ Run a builtin scanpy command or a scanpy-* subcommand. @@ -116,7 +125,10 @@ def main(argv: Optional[Sequence[str]] = None, *, check: bool = True, **runargs) `~run(['scanpy', *argv], **runargs)` """ parser = ArgumentParser( - description=("There are a few packages providing commands. " "Try e.g. `pip install scanpy-scripts`!") + description=( + "There are a few packages providing commands. " + "Try e.g. `pip install scanpy-scripts`!" + ) ) parser.set_defaults(func=parser.print_help) diff --git a/scanpy/datasets/_datasets.py b/scanpy/datasets/_datasets.py index ec909b41e6..060b00ddd3 100644 --- a/scanpy/datasets/_datasets.py +++ b/scanpy/datasets/_datasets.py @@ -136,10 +136,13 @@ def moignard15() -> AnnData: } # annotate each observation/cell adata.obs['exp_groups'] = [ - next(gname for gname in groups.keys() if sname.startswith(gname)) for sname in adata.obs_names + next(gname for gname in groups.keys() if sname.startswith(gname)) + for sname in adata.obs_names ] # fix the order and colors of names in "groups" - adata.obs['exp_groups'] = pd.Categorical(adata.obs['exp_groups'], categories=list(groups.keys())) + adata.obs['exp_groups'] = pd.Categorical( + adata.obs['exp_groups'], categories=list(groups.keys()) + ) adata.uns['exp_groups_colors'] = list(groups.values()) return adata @@ -159,7 +162,10 @@ def paul15() -> AnnData: ------- Annotated data matrix. """ - logg.warning('In Scanpy 0.*, this returned logarithmized data. ' 'Now it returns non-logarithmized data.') + logg.warning( + 'In Scanpy 0.*, this returned logarithmized data. ' + 'Now it returns non-logarithmized data.' + ) import h5py filename = settings.datasetdir / 'paul15/paul15.h5' @@ -329,7 +335,9 @@ def _download_visium_dataset( # Download spatial data tar_filename = f"{sample_id}_spatial.tar.gz" tar_pth = sample_dir / tar_filename - _utils.check_presence_download(filename=tar_pth, backup_url=url_prefix + tar_filename) + _utils.check_presence_download( + filename=tar_pth, backup_url=url_prefix + tar_filename + ) with tarfile.open(tar_pth) as f: for el in f: if not (sample_dir / el.name).exists(): @@ -403,7 +411,9 @@ def visium_sge( spaceranger_version = "1.1.0" else: spaceranger_version = "1.2.0" - _download_visium_dataset(sample_id, spaceranger_version, download_image=include_hires_tiff) + _download_visium_dataset( + sample_id, spaceranger_version, download_image=include_hires_tiff + ) if include_hires_tiff: adata = read_visium( settings.datasetdir / sample_id, diff --git a/scanpy/datasets/_ebi_expression_atlas.py b/scanpy/datasets/_ebi_expression_atlas.py index e45e73a18d..013f0aa804 100644 --- a/scanpy/datasets/_ebi_expression_atlas.py +++ b/scanpy/datasets/_ebi_expression_atlas.py @@ -86,7 +86,9 @@ def read_expression_from_archive(archive: ZipFile) -> anndata.AnnData: return adata -def ebi_expression_atlas(accession: str, *, filter_boring: bool = False) -> anndata.AnnData: +def ebi_expression_atlas( + accession: str, *, filter_boring: bool = False +) -> anndata.AnnData: """\ Load a dataset from the `EBI Single Cell Expression Atlas `__ diff --git a/scanpy/external/exporting.py b/scanpy/external/exporting.py index 9a36dce538..a405cbb365 100644 --- a/scanpy/external/exporting.py +++ b/scanpy/external/exporting.py @@ -75,16 +75,25 @@ def spring_project( embedding_method = 'X_' + embedding_method else: if embedding_method in adata.uns: - embedding_method = 'X_' + embedding_method + '_' + adata.uns[embedding_method]['params']['layout'] + embedding_method = ( + 'X_' + + embedding_method + + '_' + + adata.uns[embedding_method]['params']['layout'] + ) else: - raise ValueError('Run the specified embedding method `%s` first.' % embedding_method) + raise ValueError( + 'Run the specified embedding method `%s` first.' % embedding_method + ) coords = adata.obsm[embedding_method] # Make project directory and subplot directory (subplot has same name as project) # For now, the subplot is just all cells in adata project_dir: Path = Path(project_dir) - subplot_dir: Path = project_dir.parent if subplot_name is None else project_dir / subplot_name + subplot_dir: Path = ( + project_dir.parent if subplot_name is None else project_dir / subplot_name + ) subplot_dir.mkdir(parents=True, exist_ok=True) print(f'Writing subplot to {subplot_dir}') @@ -150,7 +159,9 @@ def spring_project( elif is_categorical(adata.obs[obs_name]): categorical_extras[obs_name] = [str(x) for x in adata.obs[obs_name]] else: - logg.warning(f'Cell grouping {obs_name!r} is not a categorical variable') + logg.warning( + f'Cell grouping {obs_name!r} is not a categorical variable' + ) if custom_color_tracks is None: for obs_name in adata.obs: if not is_categorical(adata.obs[obs_name]): @@ -164,7 +175,9 @@ def spring_project( elif not is_categorical(adata.obs[obs_name]): continuous_extras[obs_name] = np.array(adata.obs[obs_name]) else: - logg.warning(f'Custom color track {obs_name!r} is not a continuous variable') + logg.warning( + f'Custom color track {obs_name!r} is not a continuous variable' + ) # Write continuous colors continuous_extras['Uniform'] = np.zeros(E.shape[0]) @@ -178,8 +191,12 @@ def spring_project( # Write categorical data categorical_coloring_data = {} - categorical_coloring_data = _build_categ_colors(categorical_coloring_data, categorical_extras) - _write_cell_groupings(subplot_dir / 'categorical_coloring_data.json', categorical_coloring_data) + categorical_coloring_data = _build_categ_colors( + categorical_coloring_data, categorical_extras + ) + _write_cell_groupings( + subplot_dir / 'categorical_coloring_data.json', categorical_coloring_data + ) # Write graph in two formats for backwards compatibility edges = _get_edges(adata, neighbors_key) @@ -193,7 +210,10 @@ def spring_project( # Write 2-D coordinates, after adjusting to roughly match SPRING's default d3js force layout parameters coords = coords - coords.min(0)[None, :] - coords = coords * (np.array([1000, 1000]) / coords.ptp(0))[None, :] + np.array([200, -200])[None, :] + coords = ( + coords * (np.array([1000, 1000]) / coords.ptp(0))[None, :] + + np.array([200, -200])[None, :] + ) np.savetxt( subplot_dir / 'coordinates.txt', np.hstack((np.arange(E.shape[0])[:, None], coords)), @@ -323,10 +343,14 @@ def _get_color_stats_genes(color_stats, E, gene_list): for iG in range(E.shape[1]): n_nonzero = E.indptr[iG + 1] - E.indptr[iG] if n_nonzero > pctl_n: - pctls[iG] = np.percentile(E.data[E.indptr[iG] : E.indptr[iG + 1]], 100 - 100 * pctl_n / n_nonzero) + pctls[iG] = np.percentile( + E.data[E.indptr[iG] : E.indptr[iG + 1]], 100 - 100 * pctl_n / n_nonzero + ) else: pctls[iG] = 0 - color_stats[gene_list[iG]] = tuple(map(float, (means[iG], stdevs[iG], mins[iG], maxes[iG], pctls[iG]))) + color_stats[gene_list[iG]] = tuple( + map(float, (means[iG], stdevs[iG], mins[iG], maxes[iG], pctls[iG])) + ) return color_stats @@ -348,7 +372,10 @@ def _write_color_stats(filename, color_stats): def _build_categ_colors(categorical_coloring_data, cell_groupings): for k, labels in cell_groupings.items(): - label_colors = {l: _frac_to_hex(float(i) / len(set(labels))) for i, l in enumerate(list(set(labels)))} + label_colors = { + l: _frac_to_hex(float(i) / len(set(labels))) + for i, l in enumerate(list(set(labels))) + } categorical_coloring_data[k] = { 'label_colors': label_colors, 'label_list': labels, @@ -358,7 +385,9 @@ def _build_categ_colors(categorical_coloring_data, cell_groupings): def _write_cell_groupings(filename, categorical_coloring_data): with open(filename, 'w') as f: - f.write(json.dumps(categorical_coloring_data, indent=4, sort_keys=True)) # .decode('utf-8')) + f.write( + json.dumps(categorical_coloring_data, indent=4, sort_keys=True) + ) # .decode('utf-8')) def _export_PAGA_to_SPRING(adata, paga_coords, outpath): @@ -369,14 +398,18 @@ def _export_PAGA_to_SPRING(adata, paga_coords, outpath): sizes = list(adata.uns[group_key + '_sizes']) clus_labels = adata.obs[group_key].cat.codes.values - cell_groups = [[int(j) for j in np.nonzero(clus_labels == i)[0]] for i in range(len(names))] + cell_groups = [ + [int(j) for j in np.nonzero(clus_labels == i)[0]] for i in range(len(names)) + ] if group_key + '_colors' in adata.uns: colors = list(adata.uns[group_key + '_colors']) else: import scanpy.plotting.utils - scanpy.plotting.utils.add_colors_for_categorical_sample_annotation(adata, group_key) + scanpy.plotting.utils.add_colors_for_categorical_sample_annotation( + adata, group_key + ) colors = list(adata.uns[group_key + '_colors']) # retrieve edge level data @@ -398,7 +431,9 @@ def _export_PAGA_to_SPRING(adata, paga_coords, outpath): # make node list nodes = [] - for i, name, xy, color, size, cells in zip(range(len(names)), names, coords, colors, sizes, cell_groups): + for i, name, xy, color, size, cells in zip( + range(len(names)), names, coords, colors, sizes, cell_groups + ): nodes.append( { 'index': i, @@ -414,7 +449,9 @@ def _export_PAGA_to_SPRING(adata, paga_coords, outpath): links = [] for source, target, weight in zip(sources, targets, weights): if source < target and weight > min_edge_weight_save: - links.append({'source': int(source), 'target': int(target), 'weight': float(weight)}) + links.append( + {'source': int(source), 'target': int(target), 'weight': float(weight)} + ) # save data about edge weights edge_weight_meta = { @@ -527,7 +564,10 @@ def cellbrowser( try: import cellbrowser.cellbrowser as cb except ImportError: - logg.error("The package cellbrowser is not installed. " "Install with 'pip install cellbrowser' and retry.") + logg.error( + "The package cellbrowser is not installed. " + "Install with 'pip install cellbrowser' and retry." + ) raise data_dir = str(data_dir) diff --git a/scanpy/external/pl.py b/scanpy/external/pl.py index 01fac05ed6..2c4167d72f 100644 --- a/scanpy/external/pl.py +++ b/scanpy/external/pl.py @@ -176,7 +176,9 @@ def sam( try: dt = adata.obsm[projection] except KeyError: - raise ValueError('Please create a projection first using run_umap or run_tsne') + raise ValueError( + 'Please create a projection first using run_umap or run_tsne' + ) else: dt = projection @@ -185,7 +187,9 @@ def sam( axes = plt.gca() if c is None: - axes.scatter(dt[:, 0], dt[:, 1], s=s, linewidth=linewidth, edgecolor=edgecolor, **kwargs) + axes.scatter( + dt[:, 0], dt[:, 1], s=s, linewidth=linewidth, edgecolor=edgecolor, **kwargs + ) return axes if isinstance(c, str): diff --git a/scanpy/external/pp/_hashsolo.py b/scanpy/external/pp/_hashsolo.py index ca2ab5179a..ecc3e8e9ac 100644 --- a/scanpy/external/pp/_hashsolo.py +++ b/scanpy/external/pp/_hashsolo.py @@ -66,7 +66,9 @@ def gaussian_updates(data, mu_o, std_o): n = len(data) lam = 1 / np.var(data) if len(data) > 1 else lam_o lam_n = lam_o + n * lam - mu_n = (np.mean(data) * n * lam + mu_o * lam_o) / lam_n if len(data) > 0 else mu_o + mu_n = ( + (np.mean(data) * n * lam + mu_o * lam_o) / lam_n if len(data) > 0 else mu_o + ) return mu_n, (1 / (lam_n / (n + 1))) ** (1 / 2) eps = 1e-15 @@ -89,8 +91,12 @@ def gaussian_updates(data, mu_o, std_o): # barcodes with rank < k are considered to be noise global_signal_counts = np.ravel(data_sort[:, -1]) global_noise_counts = np.ravel(data_sort[:, :-number_of_non_noise_barcodes]) - global_mu_signal_o, global_sigma_signal_o = np.mean(global_signal_counts), np.std(global_signal_counts) - global_mu_noise_o, global_sigma_noise_o = np.mean(global_noise_counts), np.std(global_noise_counts) + global_mu_signal_o, global_sigma_signal_o = np.mean(global_signal_counts), np.std( + global_signal_counts + ) + global_mu_noise_o, global_sigma_noise_o = np.mean(global_noise_counts), np.std( + global_noise_counts + ) noise_params_dict = {} signal_params_dict = {} @@ -98,7 +104,9 @@ def gaussian_updates(data, mu_o, std_o): # for each barcode get empirical noise and signal distribution parameterization for x in np.arange(num_of_barcodes): sample_barcodes = data[:, x] - sample_barcodes_noise_idx = np.where(data_arg[:, :num_of_noise_barcodes] == x)[0] + sample_barcodes_noise_idx = np.where(data_arg[:, :num_of_noise_barcodes] == x)[ + 0 + ] sample_barcodes_signal_idx = np.where(data_arg[:, -1] == x) # get noise and signal counts @@ -106,8 +114,12 @@ def gaussian_updates(data, mu_o, std_o): signal_counts = sample_barcodes[sample_barcodes_signal_idx] # get parameters of distribution, assuming lognormal do update from global values - noise_param = gaussian_updates(noise_counts, global_mu_noise_o, global_sigma_noise_o) - signal_param = gaussian_updates(signal_counts, global_mu_signal_o, global_sigma_signal_o) + noise_param = gaussian_updates( + noise_counts, global_mu_noise_o, global_sigma_noise_o + ) + signal_param = gaussian_updates( + signal_counts, global_mu_signal_o, global_sigma_signal_o + ) noise_params_dict[x] = noise_param signal_params_dict[x] = signal_param @@ -115,7 +127,9 @@ def gaussian_updates(data, mu_o, std_o): counter = 0 # for each combination of noise and signal barcode calculate probiltiy of in silico and real cell hypotheses - for noise_sample_idx, signal_sample_idx in product(np.arange(num_of_barcodes), np.arange(num_of_barcodes)): + for noise_sample_idx, signal_sample_idx in product( + np.arange(num_of_barcodes), np.arange(num_of_barcodes) + ): signal_subset = data_arg[:, -1] == signal_sample_idx noise_subset = data_arg[:, -2] == noise_sample_idx subset = signal_subset & noise_subset @@ -168,9 +182,15 @@ def gaussian_updates(data, mu_o, std_o): + eps ) - probs_of_negative = np.sum([log_noise_noise_probs, log_signal_noise_probs], axis=0) - probs_of_singlet = np.sum([log_noise_noise_probs, log_signal_signal_probs], axis=0) - probs_of_doublet = np.sum([log_noise_signal_probs, log_signal_signal_probs], axis=0) + probs_of_negative = np.sum( + [log_noise_noise_probs, log_signal_noise_probs], axis=0 + ) + probs_of_singlet = np.sum( + [log_noise_noise_probs, log_signal_signal_probs], axis=0 + ) + probs_of_doublet = np.sum( + [log_noise_signal_probs, log_signal_signal_probs], axis=0 + ) log_probs_list = [probs_of_negative, probs_of_singlet, probs_of_doublet] # each cell and each hypothesis probability @@ -206,11 +226,15 @@ def _calculate_bayes_rule(data, priors, number_of_noise_barcodes): "log_likelihoods_for_each_hypothesis" key is a 2d np.array log likelihood of each hypothesis """ priors = np.array(priors) - log_likelihoods_for_each_hypothesis, _, _ = _calculate_log_likelihoods(data, number_of_noise_barcodes) + log_likelihoods_for_each_hypothesis, _, _ = _calculate_log_likelihoods( + data, number_of_noise_barcodes + ) probs_hypotheses = ( np.exp(log_likelihoods_for_each_hypothesis) * priors - / np.sum(np.multiply(np.exp(log_likelihoods_for_each_hypothesis), priors), axis=1)[:, None] + / np.sum( + np.multiply(np.exp(log_likelihoods_for_each_hypothesis), priors), axis=1 + )[:, None] ) most_likely_hypothesis = np.argmax(probs_hypotheses, axis=1) return { @@ -271,7 +295,9 @@ def hashsolo( >>> sce.pp.hashsolo(data, ['Hash1', 'Hash2', 'Hash3']) >>> data.obs.head() """ - print("Please cite HashSolo paper:\nhttps://www.cell.com/cell-systems/fulltext/S2405-4712(20)30195-2") + print( + "Please cite HashSolo paper:\nhttps://www.cell.com/cell-systems/fulltext/S2405-4712(20)30195-2" + ) data = adata.obs[cell_hashing_columns].values if not check_nonnegative_integers(data): @@ -300,39 +326,67 @@ def hashsolo( unique_cluster_features = np.unique(adata.obs[cluster_features]) for cluster_feature in unique_cluster_features: cluster_feature_bool_vector = adata.obs[cluster_features] == cluster_feature - posterior_dict = _calculate_bayes_rule(data[cluster_feature_bool_vector], priors, number_of_noise_barcodes) - results.loc[cluster_feature_bool_vector, "most_likely_hypothesis"] = posterior_dict[ - "most_likely_hypothesis" - ] - results.loc[cluster_feature_bool_vector, "cluster_feature"] = cluster_feature - results.loc[cluster_feature_bool_vector, "negative_hypothesis_probability"] = posterior_dict[ - "probs_hypotheses" - ][:, 0] - results.loc[cluster_feature_bool_vector, "singlet_hypothesis_probability"] = posterior_dict[ - "probs_hypotheses" - ][:, 1] - results.loc[cluster_feature_bool_vector, "doublet_hypothesis_probability"] = posterior_dict[ - "probs_hypotheses" - ][:, 2] + posterior_dict = _calculate_bayes_rule( + data[cluster_feature_bool_vector], priors, number_of_noise_barcodes + ) + results.loc[ + cluster_feature_bool_vector, "most_likely_hypothesis" + ] = posterior_dict["most_likely_hypothesis"] + results.loc[ + cluster_feature_bool_vector, "cluster_feature" + ] = cluster_feature + results.loc[ + cluster_feature_bool_vector, "negative_hypothesis_probability" + ] = posterior_dict["probs_hypotheses"][:, 0] + results.loc[ + cluster_feature_bool_vector, "singlet_hypothesis_probability" + ] = posterior_dict["probs_hypotheses"][:, 1] + results.loc[ + cluster_feature_bool_vector, "doublet_hypothesis_probability" + ] = posterior_dict["probs_hypotheses"][:, 2] else: posterior_dict = _calculate_bayes_rule(data, priors, number_of_noise_barcodes) - results.loc[:, "most_likely_hypothesis"] = posterior_dict["most_likely_hypothesis"] + results.loc[:, "most_likely_hypothesis"] = posterior_dict[ + "most_likely_hypothesis" + ] results.loc[:, "cluster_feature"] = 0 - results.loc[:, "negative_hypothesis_probability"] = posterior_dict["probs_hypotheses"][:, 0] - results.loc[:, "singlet_hypothesis_probability"] = posterior_dict["probs_hypotheses"][:, 1] - results.loc[:, "doublet_hypothesis_probability"] = posterior_dict["probs_hypotheses"][:, 2] - - adata.obs["most_likely_hypothesis"] = results.loc[adata.obs_names, "most_likely_hypothesis"] + results.loc[:, "negative_hypothesis_probability"] = posterior_dict[ + "probs_hypotheses" + ][:, 0] + results.loc[:, "singlet_hypothesis_probability"] = posterior_dict[ + "probs_hypotheses" + ][:, 1] + results.loc[:, "doublet_hypothesis_probability"] = posterior_dict[ + "probs_hypotheses" + ][:, 2] + + adata.obs["most_likely_hypothesis"] = results.loc[ + adata.obs_names, "most_likely_hypothesis" + ] adata.obs["cluster_feature"] = results.loc[adata.obs_names, "cluster_feature"] - adata.obs["negative_hypothesis_probability"] = results.loc[adata.obs_names, "negative_hypothesis_probability"] - adata.obs["singlet_hypothesis_probability"] = results.loc[adata.obs_names, "singlet_hypothesis_probability"] - adata.obs["doublet_hypothesis_probability"] = results.loc[adata.obs_names, "doublet_hypothesis_probability"] + adata.obs["negative_hypothesis_probability"] = results.loc[ + adata.obs_names, "negative_hypothesis_probability" + ] + adata.obs["singlet_hypothesis_probability"] = results.loc[ + adata.obs_names, "singlet_hypothesis_probability" + ] + adata.obs["doublet_hypothesis_probability"] = results.loc[ + adata.obs_names, "doublet_hypothesis_probability" + ] adata.obs["Classification"] = None - adata.obs.loc[adata.obs["most_likely_hypothesis"] == 2, "Classification"] = "Doublet" - adata.obs.loc[adata.obs["most_likely_hypothesis"] == 0, "Classification"] = "Negative" + adata.obs.loc[ + adata.obs["most_likely_hypothesis"] == 2, "Classification" + ] = "Doublet" + adata.obs.loc[ + adata.obs["most_likely_hypothesis"] == 0, "Classification" + ] = "Negative" all_sings = adata.obs["most_likely_hypothesis"] == 1 - singlet_sample_index = np.argmax(adata.obs.loc[all_sings, cell_hashing_columns].values, axis=1) - adata.obs.loc[all_sings, "Classification"] = adata.obs[cell_hashing_columns].columns[singlet_sample_index] + singlet_sample_index = np.argmax( + adata.obs.loc[all_sings, cell_hashing_columns].values, axis=1 + ) + adata.obs.loc[all_sings, "Classification"] = adata.obs[ + cell_hashing_columns + ].columns[singlet_sample_index] return adata if not inplace else None diff --git a/scanpy/external/pp/_magic.py b/scanpy/external/pp/_magic.py index aeb382733b..5690923c5b 100644 --- a/scanpy/external/pp/_magic.py +++ b/scanpy/external/pp/_magic.py @@ -150,7 +150,10 @@ def magic( start = logg.info('computing MAGIC') all_or_pca = isinstance(name_list, (str, type(None))) if all_or_pca and name_list not in {"all_genes", "pca_only", None}: - raise ValueError("Invalid string value for `name_list`: " "Only `'all_genes'` and `'pca_only'` are allowed.") + raise ValueError( + "Invalid string value for `name_list`: " + "Only `'all_genes'` and `'pca_only'` are allowed." + ) if copy is None: copy = not all_or_pca elif not all_or_pca and not copy: @@ -178,7 +181,11 @@ def magic( logg.info( ' finished', time=start, - deep=("added\n 'X_magic', PCA on MAGIC coordinates (adata.obsm)" if name_list == "pca_only" else ''), + deep=( + "added\n 'X_magic', PCA on MAGIC coordinates (adata.obsm)" + if name_list == "pca_only" + else '' + ), ) # update AnnData instance if name_list == "pca_only": diff --git a/scanpy/external/pp/_mnn_correct.py b/scanpy/external/pp/_mnn_correct.py index ff90cb05be..67ff5748ba 100644 --- a/scanpy/external/pp/_mnn_correct.py +++ b/scanpy/external/pp/_mnn_correct.py @@ -28,7 +28,11 @@ def mnn_correct( save_raw: bool = False, n_jobs: Optional[int] = None, **kwargs, -) -> Tuple[Union[np.ndarray, AnnData], List[pd.DataFrame], Optional[List[Tuple[Optional[float], int]]],]: +) -> Tuple[ + Union[np.ndarray, AnnData], + List[pd.DataFrame], + Optional[List[Tuple[Optional[float], int]]], +]: """\ Correct batch effects by matching mutual nearest neighbors [Haghverdi18]_ [Kang18]_. @@ -122,7 +126,10 @@ def mnn_correct( try: from mnnpy import mnn_correct except ImportError: - raise ImportError('Please install the package mnnpy ' '(https://github.com/chriscainx/mnnpy). ') + raise ImportError( + 'Please install the package mnnpy ' + '(https://github.com/chriscainx/mnnpy). ' + ) n_jobs = settings.n_jobs if n_jobs is None else n_jobs datas, mnn_list, angle_list = mnn_correct( diff --git a/scanpy/external/pp/_scanorama_integrate.py b/scanpy/external/pp/_scanorama_integrate.py index 0d5a058ec1..9ea4d150cd 100644 --- a/scanpy/external/pp/_scanorama_integrate.py +++ b/scanpy/external/pp/_scanorama_integrate.py @@ -113,7 +113,9 @@ def scanorama_integrate( name2idx[batch_name].append(idx) # Separate batches. - datasets_dimred = [adata.obsm[basis][name2idx[batch_name]] for batch_name in batch_names] + datasets_dimred = [ + adata.obsm[basis][name2idx[batch_name]] for batch_name in batch_names + ] # Integrate. integrated = scanorama.assemble( diff --git a/scanpy/external/pp/_scrublet.py b/scanpy/external/pp/_scrublet.py index 46774706eb..1965bb0f88 100644 --- a/scanpy/external/pp/_scrublet.py +++ b/scanpy/external/pp/_scrublet.py @@ -148,7 +148,9 @@ def scrublet( try: import scrublet as sl except ImportError: - raise ImportError('Please install scrublet: `pip install scrublet` or `conda install scrublet`.') + raise ImportError( + 'Please install scrublet: `pip install scrublet` or `conda install scrublet`.' + ) if copy: adata = adata.copy() @@ -338,7 +340,9 @@ def _scrublet_call_doublets( try: import scrublet as sl except ImportError: - raise ImportError('Please install scrublet: `pip install scrublet` or `conda install scrublet`.') + raise ImportError( + 'Please install scrublet: `pip install scrublet` or `conda install scrublet`.' + ) # Estimate n_neighbors if not provided, and create scrublet object. @@ -372,10 +376,14 @@ def _scrublet_call_doublets( if mean_center: logg.info('Embedding transcriptomes using PCA...') - sl.pipeline_pca(scrub, n_prin_comps=n_prin_comps, random_state=scrub.random_state) + sl.pipeline_pca( + scrub, n_prin_comps=n_prin_comps, random_state=scrub.random_state + ) else: logg.info('Embedding transcriptomes using Truncated SVD...') - sl.pipeline_truncated_svd(scrub, n_prin_comps=n_prin_comps, random_state=scrub.random_state) + sl.pipeline_truncated_svd( + scrub, n_prin_comps=n_prin_comps, random_state=scrub.random_state + ) # Score the doublets @@ -403,7 +411,9 @@ def _scrublet_call_doublets( 'parameters': { 'expected_doublet_rate': expected_doublet_rate, 'sim_doublet_ratio': ( - adata_sim.uns.get('scrublet', {}).get('parameters', {}).get('sim_doublet_ratio', None) + adata_sim.uns.get('scrublet', {}) + .get('parameters', {}) + .get('sim_doublet_ratio', None) ), 'n_neighbors': n_neighbors, 'random_state': random_state, @@ -411,7 +421,9 @@ def _scrublet_call_doublets( } if get_doublet_neighbor_parents: - adata_obs.uns['scrublet']['doublet_neighbor_parents'] = scrub.doublet_neighbor_parents_ + adata_obs.uns['scrublet'][ + 'doublet_neighbor_parents' + ] = scrub.doublet_neighbor_parents_ return adata_obs @@ -466,7 +478,9 @@ def scrublet_simulate_doublets( try: import scrublet as sl except ImportError: - raise ImportError('Please install scrublet: `pip install scrublet` or `conda install scrublet`.') + raise ImportError( + 'Please install scrublet: `pip install scrublet` or `conda install scrublet`.' + ) X = _get_obs_rep(adata, layer=layer) scrub = sl.Scrublet(X) diff --git a/scanpy/external/pp/_scvi.py b/scanpy/external/pp/_scvi.py index 9d963a4760..f4f75b0607 100644 --- a/scanpy/external/pp/_scvi.py +++ b/scanpy/external/pp/_scvi.py @@ -117,7 +117,9 @@ def scvi( from scvi.inference import UnsupervisedTrainer from scvi.dataset import AnnDatasetFromAnnData except ImportError: - raise ImportError("Please install scvi package from https://github.com/YosefLab/scVI") + raise ImportError( + "Please install scvi package from https://github.com/YosefLab/scVI" + ) # check if observations are unnormalized using first 10 # code from: https://github.com/theislab/dca/blob/89eee4ed01dd969b3d46e0c815382806fbfc2526/dca/io.py#L63-L69 @@ -125,7 +127,9 @@ def scvi( X_subset = adata.X[:10] else: X_subset = adata.X - norm_error = 'Make sure that the dataset (adata.X) contains unnormalized count data.' + norm_error = ( + 'Make sure that the dataset (adata.X) contains unnormalized count data.' + ) if sp.sparse.issparse(X_subset): assert (X_subset.astype(int) != X_subset).nnz == 0, norm_error else: @@ -181,7 +185,9 @@ def scvi( trainer.train(n_epochs=n_epochs, lr=lr) - full = trainer.create_posterior(trainer.model, dataset, indices=np.arange(len(dataset))) + full = trainer.create_posterior( + trainer.model, dataset, indices=np.arange(len(dataset)) + ) latent, batch_indices, labels = full.sequential().get_latent() if copy: diff --git a/scanpy/external/tl/_palantir.py b/scanpy/external/tl/_palantir.py index 8fc05de277..ba437d5f52 100644 --- a/scanpy/external/tl/_palantir.py +++ b/scanpy/external/tl/_palantir.py @@ -217,7 +217,9 @@ def palantir( # MAGIC imputation if impute_data: - imp_df = run_magic_imputation(data=adata.to_df(), dm_res=dm_res, n_steps=n_steps) + imp_df = run_magic_imputation( + data=adata.to_df(), dm_res=dm_res, n_steps=n_steps + ) adata.layers['palantir_imp'] = imp_df ( diff --git a/scanpy/external/tl/_phate.py b/scanpy/external/tl/_phate.py index f4e83d65fe..0e077b429e 100644 --- a/scanpy/external/tl/_phate.py +++ b/scanpy/external/tl/_phate.py @@ -130,7 +130,8 @@ def phate( import phate except ImportError: raise ImportError( - 'You need to install the package `phate`: please run `pip install ' '--user phate` in a terminal.' + 'You need to install the package `phate`: please run `pip install ' + '--user phate` in a terminal.' ) X_phate = phate.PHATE( n_components=n_components, diff --git a/scanpy/external/tl/_phenograph.py b/scanpy/external/tl/_phenograph.py index ef25c19e20..2a956518a2 100644 --- a/scanpy/external/tl/_phenograph.py +++ b/scanpy/external/tl/_phenograph.py @@ -195,7 +195,10 @@ def phenograph( assert phenograph.__version__ >= "1.5.3" except (ImportError, AssertionError, AttributeError): - raise ImportError("please install the latest release of phenograph:\n\t" "pip install -U PhenoGraph") + raise ImportError( + "please install the latest release of phenograph:\n\t" + "pip install -U PhenoGraph" + ) if isinstance(adata, AnnData): try: @@ -206,7 +209,11 @@ def phenograph( data = adata copy = True - comm_key = "pheno_{}".format(clustering_algo) if clustering_algo in ["louvain", "leiden"] else '' + comm_key = ( + "pheno_{}".format(clustering_algo) + if clustering_algo in ["louvain", "leiden"] + else '' + ) ig_key = "pheno_{}_ig".format("jaccard" if jaccard else "gaussian") q_key = "pheno_{}_q".format("jaccard" if jaccard else "gaussian") diff --git a/scanpy/external/tl/_trimap.py b/scanpy/external/tl/_trimap.py index 89d636e87e..5334e29fda 100644 --- a/scanpy/external/tl/_trimap.py +++ b/scanpy/external/tl/_trimap.py @@ -101,7 +101,8 @@ def trimap( X = adata.X if scp.issparse(X): raise ValueError( - 'trimap currently does not support sparse matrices. Please' 'use a dense matrix or apply pca first.' + 'trimap currently does not support sparse matrices. Please' + 'use a dense matrix or apply pca first.' ) logg.warning('`X_pca` not found. Run `sc.pp.pca` first for speedup.') X_trimap = TRIMAP( diff --git a/scanpy/external/tl/_wishbone.py b/scanpy/external/tl/_wishbone.py index c8f3415b58..681950a264 100644 --- a/scanpy/external/tl/_wishbone.py +++ b/scanpy/external/tl/_wishbone.py @@ -93,16 +93,24 @@ def wishbone( try: from wishbone.core import wishbone as c_wishbone except ImportError: - raise ImportError("\nplease install wishbone:\n\n\thttps://github.com/dpeerlab/wishbone") + raise ImportError( + "\nplease install wishbone:\n\n\thttps://github.com/dpeerlab/wishbone" + ) # Start cell index s = np.where(adata.obs_names == start_cell)[0] if len(s) == 0: - raise RuntimeError(f"Start cell {start_cell} not found in data. " "Please rerun with correct start cell.") + raise RuntimeError( + f"Start cell {start_cell} not found in data. " + "Please rerun with correct start cell." + ) if isinstance(num_waypoints, cabc.Collection): diff = np.setdiff1d(num_waypoints, adata.obs.index) if diff.size > 0: - logging.warning("Some of the specified waypoints are not in the data. " "These will be removed") + logging.warning( + "Some of the specified waypoints are not in the data. " + "These will be removed" + ) num_waypoints = diff.tolist() elif num_waypoints > adata.shape[0]: raise RuntimeError( @@ -124,7 +132,9 @@ def wishbone( # Assign results trajectory = res["Trajectory"] - trajectory = (trajectory - np.min(trajectory)) / (np.max(trajectory) - np.min(trajectory)) + trajectory = (trajectory - np.min(trajectory)) / ( + np.max(trajectory) - np.min(trajectory) + ) adata.obs['trajectory_wishbone'] = np.asarray(trajectory) # branch_ = None @@ -137,7 +147,9 @@ def _anndata_to_wishbone(adata: AnnData): from wishbone.wb import SCData, Wishbone scdata = SCData(adata.to_df()) - scdata.diffusion_eigenvectors = pd.DataFrame(adata.obsm['X_diffmap'], index=adata.obs_names) + scdata.diffusion_eigenvectors = pd.DataFrame( + adata.obsm['X_diffmap'], index=adata.obs_names + ) wb = Wishbone(scdata) wb.trajectory = adata.obs["trajectory_wishbone"] wb.branch = adata.obs["branch_wishbone"] diff --git a/scanpy/get/get.py b/scanpy/get/get.py index 5f1f818dd7..10cde3f9e5 100644 --- a/scanpy/get/get.py +++ b/scanpy/get/get.py @@ -147,21 +147,26 @@ def _check_indices( if key in dim_df.columns: col_keys.append(key) if key in alt_names.index: - raise KeyError(f"The key '{key}' is found in both adata.{dim} and {alt_repr}.{alt_search_repr}.") + raise KeyError( + f"The key '{key}' is found in both adata.{dim} and {alt_repr}.{alt_search_repr}." + ) elif key in alt_names.index: val = alt_names[key] if isinstance(val, pd.Series): # while var_names must be unique, adata.var[gene_symbols] does not # It's still ambiguous to refer to a duplicated entry though. assert alias_index is not None - raise KeyError(f"Found duplicate entries for '{key}' in {alt_repr}.{alt_search_repr}.") + raise KeyError( + f"Found duplicate entries for '{key}' in {alt_repr}.{alt_search_repr}." + ) index_keys.append(val) index_aliases.append(key) else: not_found.append(key) if len(not_found) > 0: raise KeyError( - f"Could not find keys '{not_found}' in columns of `adata.{dim}` or in" f" {alt_repr}.{alt_search_repr}." + f"Could not find keys '{not_found}' in columns of `adata.{dim}` or in" + f" {alt_repr}.{alt_search_repr}." ) return col_keys, index_keys, index_aliases @@ -253,7 +258,9 @@ def obs_df( >>> mean, var = grouped.mean(), grouped.var() """ if use_raw: - assert layer is None, "Cannot specify use_raw=True and a layer at the same time." + assert ( + layer is None + ), "Cannot specify use_raw=True and a layer at the same time." var = adata.raw.var else: var = adata.var @@ -400,7 +407,8 @@ def _get_obs_rep(adata, *, use_raw=False, layer=None, obsm=None, obsp=None): return adata.obsp[obsp] else: assert False, ( - "That was unexpected. Please report this bug at:\n\n\t" " https://github.com/theislab/scanpy/issues" + "That was unexpected. Please report this bug at:\n\n\t" + " https://github.com/theislab/scanpy/issues" ) @@ -426,5 +434,6 @@ def _set_obs_rep(adata, val, *, use_raw=False, layer=None, obsm=None, obsp=None) adata.obsp[obsp] = val else: assert False, ( - "That was unexpected. Please report this bug at:\n\n\t" " https://github.com/theislab/scanpy/issues" + "That was unexpected. Please report this bug at:\n\n\t" + " https://github.com/theislab/scanpy/issues" ) diff --git a/scanpy/logging.py b/scanpy/logging.py index ab4b522a12..5226817846 100644 --- a/scanpy/logging.py +++ b/scanpy/logging.py @@ -84,7 +84,9 @@ def _set_log_level(settings, level: int): class _LogFormatter(logging.Formatter): - def __init__(self, fmt='{levelname}: {message}', datefmt='%Y-%m-%d %H:%M', style='{'): + def __init__( + self, fmt='{levelname}: {message}', datefmt='%Y-%m-%d %H:%M', style='{' + ): super().__init__(fmt, datefmt, style) def format(self, record: logging.LogRecord): @@ -98,9 +100,13 @@ def format(self, record: logging.LogRecord): if record.time_passed: # strip microseconds if record.time_passed.microseconds: - record.time_passed = timedelta(seconds=int(record.time_passed.total_seconds())) + record.time_passed = timedelta( + seconds=int(record.time_passed.total_seconds()) + ) if '{time_passed}' in record.msg: - record.msg = record.msg.replace('{time_passed}', str(record.time_passed)) + record.msg = record.msg.replace( + '{time_passed}', str(record.time_passed) + ) else: self._style._fmt += ' ({time_passed})' if record.deep: diff --git a/scanpy/neighbors/__init__.py b/scanpy/neighbors/__init__.py index b50fc39e62..3f56887cae 100644 --- a/scanpy/neighbors/__init__.py +++ b/scanpy/neighbors/__init__.py @@ -16,12 +16,16 @@ from .. import settings N_DCS = 15 # default number of diffusion components -N_PCS = settings.N_PCS # Backwards compat, constants should be defined in only one place. +N_PCS = ( + settings.N_PCS +) # Backwards compat, constants should be defined in only one place. _Method = Literal['umap', 'gauss', 'rapids'] _MetricFn = Callable[[np.ndarray, np.ndarray], float] # from sklearn.metrics.pairwise_distances.__doc__: -_MetricSparseCapable = Literal['cityblock', 'cosine', 'euclidean', 'l1', 'l2', 'manhattan'] +_MetricSparseCapable = Literal[ + 'cityblock', 'cosine', 'euclidean', 'l1', 'l2', 'manhattan' +] _MetricScipySpatial = Literal[ 'braycurtis', 'canberra', @@ -333,7 +337,9 @@ def compute_neighbors_rapids(X: np.ndarray, n_neighbors: int): return knn_indices, np.sqrt(knn_distsq) # cuml uses sqeuclidean metric so take sqrt -def _get_sparse_matrix_from_indices_distances_umap(knn_indices, knn_dists, n_obs, n_neighbors): +def _get_sparse_matrix_from_indices_distances_umap( + knn_indices, knn_dists, n_obs, n_neighbors +): rows = np.zeros((n_obs * n_neighbors), dtype=np.int64) cols = np.zeros((n_obs * n_neighbors), dtype=np.int64) vals = np.zeros((n_obs * n_neighbors), dtype=np.float64) @@ -395,12 +401,16 @@ def _compute_connectivities_umap( # In umap-learn 0.4, this returns (result, sigmas, rhos) connectivities = connectivities[0] - distances = _get_sparse_matrix_from_indices_distances_umap(knn_indices, knn_dists, n_obs, n_neighbors) + distances = _get_sparse_matrix_from_indices_distances_umap( + knn_indices, knn_dists, n_obs, n_neighbors + ) return distances, connectivities.tocsr() -def _get_sparse_matrix_from_indices_distances_numpy(indices, distances, n_obs, n_neighbors): +def _get_sparse_matrix_from_indices_distances_numpy( + indices, distances, n_obs, n_neighbors +): n_nonzero = n_obs * n_neighbors indptr = np.arange(0, n_nonzero + 1, n_neighbors) D = csr_matrix( @@ -429,7 +439,9 @@ def _get_indices_distances_from_sparse_matrix(D, n_neighbors: int): if len(neighbors[1]) > n_neighbors_m1: sorted_indices = np.argsort(D[i][neighbors].A1)[:n_neighbors_m1] indices[i, 1:] = neighbors[1][sorted_indices] - distances[i, 1:] = D[i][neighbors[0][sorted_indices], neighbors[1][sorted_indices]] + distances[i, 1:] = D[i][ + neighbors[0][sorted_indices], neighbors[1][sorted_indices] + ] else: indices[i, 1:] = neighbors[1] distances[i, 1:] = D[i][neighbors] @@ -463,7 +475,9 @@ def _make_forest_dict(forest): props = ('hyperplanes', 'offsets', 'children', 'indices') for prop in props: d[prop] = {} - sizes = np.fromiter((getattr(tree, prop).shape[0] for tree in forest), dtype=int) + sizes = np.fromiter( + (getattr(tree, prop).shape[0] for tree in forest), dtype=int + ) d[prop]['start'] = np.zeros_like(sizes) if prop == 'offsets': dims = sizes.sum() @@ -591,9 +605,15 @@ def count_nonzero(a: Union[np.ndarray, csr_matrix]) -> int: # estimating n_neighbors if self._connectivities is None: - self.n_neighbors = int(count_nonzero(self._distances) / self._distances.shape[0]) + self.n_neighbors = int( + count_nonzero(self._distances) / self._distances.shape[0] + ) else: - self.n_neighbors = int(count_nonzero(self._connectivities) / self._connectivities.shape[0] / 2) + self.n_neighbors = int( + count_nonzero(self._connectivities) + / self._connectivities.shape[0] + / 2 + ) info_str += '`.distances` `.connectivities` ' self._number_connected_components = 1 if issparse(self._connectivities): @@ -608,7 +628,9 @@ def count_nonzero(a: Union[np.ndarray, csr_matrix]) -> int: if n_dcs > len(self._eigen_values): raise ValueError( 'Cannot instantiate using `n_dcs`={}. ' - 'Compute diffmap/spectrum with more components first.'.format(n_dcs) + 'Compute diffmap/spectrum with more components first.'.format( + n_dcs + ) ) self._eigen_values = self._eigen_values[:n_dcs] self._eigen_basis = self._eigen_basis[:, :n_dcs] @@ -733,7 +755,9 @@ def compute_neighbors( if method == 'umap' and not knn: raise ValueError('`method = \'umap\' only with `knn = True`.') if method == 'rapids' and metric != 'euclidean': - raise ValueError("`method` 'rapids' only supports the 'euclidean' `metric`.") + raise ValueError( + "`method` 'rapids' only supports the 'euclidean' `metric`." + ) if method not in {'umap', 'gauss', 'rapids'}: raise ValueError("`method` needs to be 'umap', 'gauss', or 'rapids'.") if self._adata.shape[0] >= 10000 and not knn: @@ -744,10 +768,14 @@ def compute_neighbors( self.knn = knn X = _choose_representation(self._adata, use_rep=use_rep, n_pcs=n_pcs) # neighbor search - use_dense_distances = (metric == 'euclidean' and X.shape[0] < 8192) or knn == False + use_dense_distances = ( + metric == 'euclidean' and X.shape[0] < 8192 + ) or knn == False if use_dense_distances: _distances = pairwise_distances(X, metric=metric, **metric_kwds) - knn_indices, knn_distances = _get_indices_distances_from_dense_matrix(_distances, n_neighbors) + knn_indices, knn_distances = _get_indices_distances_from_dense_matrix( + _distances, n_neighbors + ) if knn: self._distances = _get_sparse_matrix_from_indices_distances_numpy( knn_indices, knn_distances, X.shape[0], n_neighbors @@ -800,10 +828,14 @@ def _compute_connectivities_diffmap(self, density_normalize=True): # init distances if self.knn: Dsq = self._distances.power(2) - indices, distances_sq = _get_indices_distances_from_sparse_matrix(Dsq, self.n_neighbors) + indices, distances_sq = _get_indices_distances_from_sparse_matrix( + Dsq, self.n_neighbors + ) else: Dsq = np.power(self._distances, 2) - indices, distances_sq = _get_indices_distances_from_dense_matrix(Dsq, self.n_neighbors) + indices, distances_sq = _get_indices_distances_from_dense_matrix( + Dsq, self.n_neighbors + ) # exclude the first point, the 0th neighbor indices = indices[:, 1:] @@ -843,7 +875,9 @@ def _compute_connectivities_diffmap(self, density_normalize=True): # set all entries that are not nearest neighbors to zero W[mask == False] = 0 else: - W = Dsq.copy() # need to copy the distance matrix here; what follows is inplace + W = ( + Dsq.copy() + ) # need to copy the distance matrix here; what follows is inplace for i in range(len(Dsq.indptr[:-1])): row = Dsq.indices[Dsq.indptr[i] : Dsq.indptr[i + 1]] num = 2 * sigmas[i] * sigmas[row] @@ -945,12 +979,17 @@ def compute_eigen( which = 'LM' if sort == 'decrease' else 'SM' # it pays off to increase the stability with a bit more precision matrix = matrix.astype(np.float64) - evals, evecs = scipy.sparse.linalg.eigsh(matrix, k=n_comps, which=which, ncv=ncv) + evals, evecs = scipy.sparse.linalg.eigsh( + matrix, k=n_comps, which=which, ncv=ncv + ) evals, evecs = evals.astype(np.float32), evecs.astype(np.float32) if sort == 'decrease': evals = evals[::-1] evecs = evecs[:, ::-1] - logg.info(' eigenvalues of transition matrix\n' ' {}'.format(str(evals).replace('\n', '\n '))) + logg.info( + ' eigenvalues of transition matrix\n' + ' {}'.format(str(evals).replace('\n', '\n ')) + ) if self._number_connected_components > len(evals) / 2: logg.warning('Transition matrix has many disconnected components!') self._eigen_values = evals @@ -984,7 +1023,12 @@ def _get_dpt_row(self, i): label = self._connected_components[1][i] mask = self._connected_components[1] == label row = sum( - (self.eigen_values[l] / (1 - self.eigen_values[l]) * (self.eigen_basis[i, l] - self.eigen_basis[:, l])) ** 2 + ( + self.eigen_values[l] + / (1 - self.eigen_values[l]) + * (self.eigen_basis[i, l] - self.eigen_basis[:, l]) + ) + ** 2 # account for float32 precision for l in range(0, self.eigen_values.size) if self.eigen_values[l] < 0.9994 @@ -1021,7 +1065,9 @@ def _set_iroot_via_xroot(self, xroot): condition, only relevant for computing pseudotime. """ if self._adata.shape[1] != xroot.size: - raise ValueError('The root vector you provided does not have the ' 'correct dimension.') + raise ValueError( + 'The root vector you provided does not have the ' 'correct dimension.' + ) # this is the squared distance dsqroot = 1e10 iroot = 0 diff --git a/scanpy/plotting/_anndata.py b/scanpy/plotting/_anndata.py index a60711b5cc..6456f4857e 100755 --- a/scanpy/plotting/_anndata.py +++ b/scanpy/plotting/_anndata.py @@ -139,7 +139,10 @@ def scatter( # store .uns annotations that were added to the new adata object adata.uns = adata_T.uns return axs - raise ValueError('`x`, `y`, and potential `color` inputs must all ' 'come from either `.obs` or `.var`') + raise ValueError( + '`x`, `y`, and potential `color` inputs must all ' + 'come from either `.obs` or `.var`' + ) def _scatter_obs( @@ -177,29 +180,43 @@ def _scatter_obs( use_raw = _check_use_raw(adata, use_raw) # Process layers - if layers in ['X', None] or (isinstance(layers, str) and layers in adata.layers.keys()): + if layers in ['X', None] or ( + isinstance(layers, str) and layers in adata.layers.keys() + ): layers = (layers, layers, layers) elif isinstance(layers, cabc.Collection) and len(layers) == 3: layers = tuple(layers) for layer in layers: if layer not in adata.layers.keys() and layer not in ['X', None]: - raise ValueError('`layers` should have elements that are ' 'either None or in adata.layers.keys().') + raise ValueError( + '`layers` should have elements that are ' + 'either None or in adata.layers.keys().' + ) else: raise ValueError( - "`layers` should be a string or a collection of strings " f"with length 3, had value '{layers}'" + "`layers` should be a string or a collection of strings " + f"with length 3, had value '{layers}'" ) if use_raw and layers not in [('X', 'X', 'X'), (None, None, None)]: ValueError('`use_raw` must be `False` if layers are used.') if legend_loc not in VALID_LEGENDLOCS: - raise ValueError(f'Invalid `legend_loc`, need to be one of: {VALID_LEGENDLOCS}.') + raise ValueError( + f'Invalid `legend_loc`, need to be one of: {VALID_LEGENDLOCS}.' + ) if components is None: components = '1,2' if '2d' in projection else '1,2,3' if isinstance(components, str): components = components.split(',') components = np.array(components).astype(int) - 1 # color can be a obs column name or a matplotlib color specification - keys = ['grey'] if color is None else [color] if isinstance(color, str) or is_color_like(color) else color + keys = ( + ['grey'] + if color is None + else [color] + if isinstance(color, str) or is_color_like(color) + else color + ) if title is not None and isinstance(title, str): title = [title] highlights = adata.uns['highlights'] if 'highlights' in adata.uns else [] @@ -213,7 +230,9 @@ def _scatter_obs( if basis == 'diffmap': components -= 1 except KeyError: - raise KeyError(f'compute coordinates using visualization tool {basis} first') + raise KeyError( + f'compute coordinates using visualization tool {basis} first' + ) elif x is not None and y is not None: if use_raw: if x in adata.obs.columns: @@ -313,7 +332,9 @@ def _scatter_obs( if legend_loc == 'right margin': right_margin = 0.5 if title is None and keys[0] is not None: - title = [key.replace('_', ' ') if not is_color_like(key) else '' for key in keys] + title = [ + key.replace('_', ' ') if not is_color_like(key) else '' for key in keys + ] axs = scatter_base( Y, @@ -373,10 +394,13 @@ def add_centroid(centroids, name, Y, mask): for name in groups: if name not in set(adata.obs[key].cat.categories): raise ValueError( - f'{name!r} is invalid! specify valid name, ' f'one of {adata.obs[key].cat.categories}' + f'{name!r} is invalid! specify valid name, ' + f'one of {adata.obs[key].cat.categories}' ) else: - iname = np.flatnonzero(adata.obs[key].cat.categories.values == name)[0] + iname = np.flatnonzero( + adata.obs[key].cat.categories.values == name + )[0] mask = scatter_group( axs[ikey], key, @@ -407,7 +431,9 @@ def add_centroid(centroids, name, Y, mask): if legend_fontweight is None: legend_fontweight = 'bold' if legend_fontoutline is not None: - path_effect = [patheffects.withStroke(linewidth=legend_fontoutline, foreground='w')] + path_effect = [ + patheffects.withStroke(linewidth=legend_fontoutline, foreground='w') + ] else: path_effect = None for name, pos in centroids.items(): @@ -449,7 +475,9 @@ def add_centroid(centroids, name, Y, mask): fontsize=legend_fontsize, ) elif legend_loc != 'none': - legend = axs[ikey].legend(frameon=False, loc=legend_loc, fontsize=legend_fontsize) + legend = axs[ikey].legend( + frameon=False, loc=legend_loc, fontsize=legend_fontsize + ) if legend is not None: for handle in legend.legendHandles: handle.set_sizes([300.0]) @@ -513,7 +541,11 @@ def ranking( if log: scores = np.log(scores) if labels is None: - labels = adata.var_names if attr in {'var', 'varm'} else np.arange(scores.shape[0]).astype(str) + labels = ( + adata.var_names + if attr in {'var', 'varm'} + else np.arange(scores.shape[0]).astype(str) + ) if isinstance(labels, str): labels = [labels + str(i + 1) for i in range(scores.shape[0])] if n_panels <= 5: @@ -665,9 +697,14 @@ def violin( ylabel = [ylabel] * (1 if groupby is None else len(keys)) if groupby is None: if len(ylabel) != 1: - raise ValueError(f'Expected number of y-labels to be `1`, found `{len(ylabel)}`.') + raise ValueError( + f'Expected number of y-labels to be `1`, found `{len(ylabel)}`.' + ) elif len(ylabel) != len(keys): - raise ValueError(f'Expected number of y-labels to be `{len(keys)}`, ' f'found `{len(ylabel)}`.') + raise ValueError( + f'Expected number of y-labels to be `{len(keys)}`, ' + f'found `{len(ylabel)}`.' + ) if groupby is not None: obs_df = get.obs_df(adata, keys=[groupby] + keys, layer=layer, use_raw=use_raw) @@ -678,7 +715,9 @@ def violin( f'but is of dtype {adata.obs[groupby].dtype}.' ) _utils.add_colors_for_categorical_sample_annotation(adata, groupby) - kwds['palette'] = dict(zip(obs_df[groupby].cat.categories, adata.uns[f'{groupby}_colors'])) + kwds['palette'] = dict( + zip(obs_df[groupby].cat.categories, adata.uns[f'{groupby}_colors']) + ) else: obs_df = get.obs_df(adata, keys=keys, layer=layer, use_raw=use_raw) if groupby is None: @@ -1001,7 +1040,9 @@ def heatmap( # reorder groupby colors if groupby_colors is not None: - groupby_colors = [groupby_colors[x] for x in dendro_data['categories_idx_ordered']] + groupby_colors = [ + groupby_colors[x] for x in dendro_data['categories_idx_ordered'] + ] if show_gene_labels is None: if len(var_names) <= 50: @@ -1090,10 +1131,14 @@ def heatmap( labels, groupby_cmap, norm, - ) = _plot_categories_as_colorblocks(groupby_ax, obs_tidy, colors=groupby_colors, orientation='left') + ) = _plot_categories_as_colorblocks( + groupby_ax, obs_tidy, colors=groupby_colors, orientation='left' + ) # add lines to main heatmap - line_positions = np.cumsum(obs_tidy.index.value_counts(sort=False))[:-1] - 0.5 + line_positions = ( + np.cumsum(obs_tidy.index.value_counts(sort=False))[:-1] - 0.5 + ) heatmap_ax.hlines( line_positions, -0.5, @@ -1106,7 +1151,9 @@ def heatmap( if dendrogram: dendro_ax = fig.add_subplot(axs[1, 2], sharey=heatmap_ax) - _plot_dendrogram(dendro_ax, adata, groupby, ticks=ticks, dendrogram_key=dendrogram) + _plot_dendrogram( + dendro_ax, adata, groupby, ticks=ticks, dendrogram_key=dendrogram + ) # plot group legends on top of heatmap_ax (if given) if var_group_positions is not None and len(var_group_positions) > 0: @@ -1185,9 +1232,13 @@ def heatmap( labels, groupby_cmap, norm, - ) = _plot_categories_as_colorblocks(groupby_ax, obs_tidy, colors=groupby_colors, orientation='bottom') + ) = _plot_categories_as_colorblocks( + groupby_ax, obs_tidy, colors=groupby_colors, orientation='bottom' + ) # add lines to main heatmap - line_positions = np.cumsum(obs_tidy.index.value_counts(sort=False))[:-1] - 0.5 + line_positions = ( + np.cumsum(obs_tidy.index.value_counts(sort=False))[:-1] - 0.5 + ) heatmap_ax.vlines( line_positions, -0.5, @@ -1213,13 +1264,17 @@ def heatmap( if var_group_positions is not None and len(var_group_positions) > 0: gene_groups_ax = fig.add_subplot(axs[1, 1]) arr = [] - for idx, (label, pos) in enumerate(zip(var_group_labels, var_group_positions)): + for idx, (label, pos) in enumerate( + zip(var_group_labels, var_group_positions) + ): if var_groups_subset_of_groupby: label_code = label2code[label] else: label_code = idx arr += [label_code] * (pos[1] + 1 - pos[0]) - gene_groups_ax.imshow(np.array([arr]).T, aspect='auto', cmap=groupby_cmap, norm=norm) + gene_groups_ax.imshow( + np.array([arr]).T, aspect='auto', cmap=groupby_cmap, norm=norm + ) gene_groups_ax.axis('off') # plot colorbar @@ -1346,7 +1401,9 @@ def tracksplot( ) categories = [categories[x] for x in dendro_data['categories_idx_ordered']] - groupby_colors = [groupby_colors[x] for x in dendro_data['categories_idx_ordered']] + groupby_colors = [ + groupby_colors[x] for x in dendro_data['categories_idx_ordered'] + ] obs_tidy = obs_tidy.sort_index() @@ -1437,7 +1494,9 @@ def tracksplot( # the ax to plot the groupby categories is split to add a small space # between the rest of the plot and the categories - axs2 = gridspec.GridSpecFromSubplotSpec(2, 1, subplot_spec=axs[num_rows - 1, 0], height_ratios=[1, 1]) + axs2 = gridspec.GridSpecFromSubplotSpec( + 2, 1, subplot_spec=axs[num_rows - 1, 0], height_ratios=[1, 1] + ) groupby_ax = fig.add_subplot(axs2[1]) @@ -1468,7 +1527,9 @@ def tracksplot( for idx, pos in enumerate(var_group_positions): arr += [idx] * (pos[1] + 1 - pos[0]) - gene_groups_ax.imshow(np.array([arr]).T, aspect='auto', cmap=groupby_cmap, norm=norm) + gene_groups_ax.imshow( + np.array([arr]).T, aspect='auto', cmap=groupby_cmap, norm=norm + ) gene_groups_ax.axis('off') return_ax_dict = {'track_axes': axs_list, 'groupby_ax': groupby_ax} @@ -1769,7 +1830,10 @@ def _prepare_dataframe( f'Given {group}, is not in observations: {adata.obs_keys()}' + msg ) if group in adata.obs.keys() and group == adata.obs.index.name: - raise ValueError(f'Given group {group} is both and index and a column level, ' 'which is ambiguous.') + raise ValueError( + f'Given group {group} is both and index and a column level, ' + 'which is ambiguous.' + ) if group == adata.obs.index.name: groupby_index = group if groupby_index is not None: @@ -1778,7 +1842,9 @@ def _prepare_dataframe( groupby = groupby.copy() # copy to not modify user passed parameter groupby.remove(groupby_index) keys = list(groupby) + list(np.unique(var_names)) - obs_tidy = get.obs_df(adata, keys=keys, layer=layer, use_raw=use_raw, gene_symbols=gene_symbols) + obs_tidy = get.obs_df( + adata, keys=keys, layer=layer, use_raw=use_raw, gene_symbols=gene_symbols + ) assert np.all(np.array(keys) == np.array(obs_tidy.columns)) if groupby_index is not None: @@ -1939,7 +2005,9 @@ def _plot_gene_groups_brackets( # remove y ticks gene_groups_ax.tick_params(axis='y', left=False, labelleft=False) # remove x ticks and labels - gene_groups_ax.tick_params(axis='x', bottom=False, labelbottom=False, labeltop=False) + gene_groups_ax.tick_params( + axis='x', bottom=False, labelbottom=False, labeltop=False + ) def _reorder_categories_after_dendrogram( @@ -2015,7 +2083,9 @@ def _reorder_categories_after_dendrogram( position = var_group_positions[idx] _var_names = var_names[position[0] : position[1] + 1] var_names_idx_ordered.extend(range(position[0], position[1] + 1)) - positions_ordered.append((position_start, position_start + len(_var_names) - 1)) + positions_ordered.append( + (position_start, position_start + len(_var_names) - 1) + ) position_start += len(_var_names) labels_ordered.append(var_group_labels[idx]) var_group_labels = labels_ordered @@ -2073,7 +2143,8 @@ def _get_dendrogram_key(adata, dendrogram_key, groupby): if 'dendrogram_info' not in adata.uns[dendrogram_key]: raise ValueError( - f"The given dendrogram key ({dendrogram_key!r}) does not contain " "valid dendrogram information." + f"The given dendrogram key ({dendrogram_key!r}) does not contain " + "valid dendrogram information." ) return dendrogram_key @@ -2145,7 +2216,9 @@ def translate_pos(pos_list, new_ticks, old_ticks): old_max = old_ticks[idx_next] new_min = new_ticks[idx_prev] new_max = new_ticks[idx_next] - new_x_val = ((x_val - old_min) / (old_max - old_min)) * (new_max - new_min) + new_min + new_x_val = ((x_val - old_min) / (old_max - old_min)) * ( + new_max - new_min + ) + new_min new_xs.append(new_x_val) return new_xs @@ -2157,7 +2230,10 @@ def translate_pos(pos_list, new_ticks, old_ticks): orig_ticks = np.arange(5, len(leaves) * 10 + 5, 10).astype(float) # check that ticks has the same length as orig_ticks if ticks is not None and len(orig_ticks) != len(ticks): - logg.warning("ticks argument does not have the same size as orig_ticks. " "The argument will be ignored") + logg.warning( + "ticks argument does not have the same size as orig_ticks. " + "The argument will be ignored" + ) ticks = None for xs, ys in zip(icoord, dcoord): @@ -2187,7 +2263,9 @@ def translate_pos(pos_list, new_ticks, old_ticks): dendro_ax.tick_params(labeltop=True, labelbottom=False) if remove_labels: - dendro_ax.tick_params(labelbottom=False, labeltop=False, labelleft=False, labelright=False) + dendro_ax.tick_params( + labelbottom=False, labeltop=False, labelleft=False, labelright=False + ) dendro_ax.grid(False) @@ -2238,7 +2316,9 @@ def _plot_categories_as_colorblocks( ticks = [] # list of centered position of the labels labels = [] label2code = {} # dictionary of numerical values asigned to each label - for code, (label, value) in enumerate(obs_tidy.index.value_counts(sort=False).iteritems()): + for code, (label, value) in enumerate( + obs_tidy.index.value_counts(sort=False).iteritems() + ): ticks.append(value_sum + (value / 2)) labels.append(label) value_sum += value diff --git a/scanpy/plotting/_baseplot_class.py b/scanpy/plotting/_baseplot_class.py index a8872b2c32..6c7e417402 100644 --- a/scanpy/plotting/_baseplot_class.py +++ b/scanpy/plotting/_baseplot_class.py @@ -94,7 +94,11 @@ def __init__( self.var_group_rotation = var_group_rotation self.width, self.height = figsize if figsize is not None else (None, None) - self.has_var_groups = True if var_group_positions is not None and len(var_group_positions) > 0 else False + self.has_var_groups = ( + True + if var_group_positions is not None and len(var_group_positions) > 0 + else False + ) self._update_var_groups() @@ -109,7 +113,10 @@ def __init__( gene_symbols=gene_symbols, ) if len(self.categories) > self.MAX_NUM_CATEGORIES: - warn(f"Over {self.MAX_NUM_CATEGORIES} categories found. " "Plot would be very large.") + warn( + f"Over {self.MAX_NUM_CATEGORIES} categories found. " + "Plot would be very large." + ) if categories_order is not None: if set(self.obs_tidy.index.categories) != set(categories_order): @@ -240,7 +247,10 @@ def add_dendrogram( if self.groupby is None or len(self.categories) <= 2: # dendrogram can only be computed between groupby categories - logg.warning("Dendrogram not added. Dendrogram is added only " "when the number of categories to plot > 2") + logg.warning( + "Dendrogram not added. Dendrogram is added only " + "when the number of categories to plot > 2" + ) return self self.group_extra_size = size @@ -391,7 +401,9 @@ def get_axes(self): self.make_figure() return self.ax_dict - def _plot_totals(self, total_barplot_ax: Axes, orientation: Literal['top', 'right']): + def _plot_totals( + self, total_barplot_ax: Axes, orientation: Literal['top', 'right'] + ): """ Makes the bar plot for totals """ @@ -486,7 +498,9 @@ def _plot_colorbar(self, color_legend_ax: Axes, normalize): cmap = pl.get_cmap(self.cmap) import matplotlib.colorbar - matplotlib.colorbar.ColorbarBase(color_legend_ax, orientation='horizontal', cmap=cmap, norm=normalize) + matplotlib.colorbar.ColorbarBase( + color_legend_ax, orientation='horizontal', cmap=cmap, norm=normalize + ) color_legend_ax.set_title(self.color_legend_title, fontsize='small') @@ -505,7 +519,9 @@ def _plot_legend(self, legend_ax, return_ax_dict, normalize): self.height - legend_height, legend_height, ] - fig, legend_gs = make_grid_spec(legend_ax, nrows=2, ncols=1, height_ratios=height_ratios) + fig, legend_gs = make_grid_spec( + legend_ax, nrows=2, ncols=1, height_ratios=height_ratios + ) color_legend_ax = fig.add_subplot(legend_gs[1]) @@ -547,7 +563,9 @@ def _mainplot(self, ax): ax.set_ylim(len(y_labels), 0) ax.set_xlim(0, len(x_labels)) - normalize = matplotlib.colors.Normalize(vmin=self.kwds.get('vmin'), vmax=self.kwds.get('vmax')) + normalize = matplotlib.colors.Normalize( + vmin=self.kwds.get('vmin'), vmax=self.kwds.get('vmax') + ) return normalize @@ -578,7 +596,9 @@ def make_figure(self): if self.height is None: mainplot_height = len(self.categories) * category_height - mainplot_width = len(self.var_names) * category_width + self.group_extra_size + mainplot_width = ( + len(self.var_names) * category_width + self.group_extra_size + ) if self.are_axes_swapped: mainplot_height, mainplot_width = mainplot_width, mainplot_height @@ -843,7 +863,9 @@ def _format_first_three_categories(_categories): position = self.var_group_positions[idx] _var_names = self.var_names[position[0] : position[1] + 1] var_names_idx_ordered.extend(range(position[0], position[1] + 1)) - positions_ordered.append((position_start, position_start + len(_var_names) - 1)) + positions_ordered.append( + (position_start, position_start + len(_var_names) - 1) + ) position_start += len(_var_names) labels_ordered.append(self.var_group_labels[idx]) self.var_group_labels = labels_ordered @@ -989,7 +1011,9 @@ def _plot_var_groups_brackets( # remove y ticks gene_groups_ax.tick_params(axis='y', left=False, labelleft=False) # remove x ticks and labels - gene_groups_ax.tick_params(axis='x', bottom=False, labelbottom=False, labeltop=False) + gene_groups_ax.tick_params( + axis='x', bottom=False, labelbottom=False, labeltop=False + ) def _update_var_groups(self): """ diff --git a/scanpy/plotting/_dotplot.py b/scanpy/plotting/_dotplot.py index 9e7ea36355..c63f0b46e5 100644 --- a/scanpy/plotting/_dotplot.py +++ b/scanpy/plotting/_dotplot.py @@ -156,12 +156,16 @@ def __init__( # of values >expression_cutoff, and divide the result by the total number of # values in the group (given by `count()`) if dot_size_df is None: - dot_size_df = obs_bool.groupby(level=0).sum() / obs_bool.groupby(level=0).count() + dot_size_df = ( + obs_bool.groupby(level=0).sum() / obs_bool.groupby(level=0).count() + ) if dot_color_df is None: # 2. compute mean expression value value if mean_only_expressed: - dot_color_df = self.obs_tidy.mask(~obs_bool).groupby(level=0).mean().fillna(0) + dot_color_df = ( + self.obs_tidy.mask(~obs_bool).groupby(level=0).mean().fillna(0) + ) else: dot_color_df = self.obs_tidy.groupby(level=0).mean() @@ -192,7 +196,9 @@ def __init__( # with df[['a', 'a', 'b']], results in a df with columns: # ['a', 'a', 'a', 'a', 'b'] - unique_var_names, unique_idx = np.unique(dot_color_df.columns, return_index=True) + unique_var_names, unique_idx = np.unique( + dot_color_df.columns, return_index=True + ) # remove duplicate columns if len(unique_var_names) != len(self.var_names): dot_color_df = dot_color_df.iloc[:, unique_idx] @@ -432,11 +438,15 @@ def _plot_size_legend(self, size_legend_ax: Axes): zorder=100, ) size_legend_ax.set_xticks(np.arange(len(size)) + 0.5) - labels = ["{}".format(np.round((x * 100), decimals=0).astype(int)) for x in size_range] + labels = [ + "{}".format(np.round((x * 100), decimals=0).astype(int)) for x in size_range + ] size_legend_ax.set_xticklabels(labels, fontsize='small') # remove y ticks and labels - size_legend_ax.tick_params(axis='y', left=False, labelleft=False, labelright=False) + size_legend_ax.tick_params( + axis='y', left=False, labelleft=False, labelright=False + ) # remove surrounding lines size_legend_ax.spines['right'].set_visible(False) @@ -473,7 +483,9 @@ def _plot_legend(self, legend_ax, return_ax_dict, normalize): spacer_height, cbar_legend_height, ] - fig, legend_gs = make_grid_spec(legend_ax, nrows=4, ncols=1, height_ratios=height_ratios) + fig, legend_gs = make_grid_spec( + legend_ax, nrows=4, ncols=1, height_ratios=height_ratios + ) if self.show_size_legend: size_legend_ax = fig.add_subplot(legend_gs[1]) @@ -619,7 +631,8 @@ def _dotplot( ) assert list(dot_size.columns) == list(dot_color.columns), ( - 'please check that the dot_size ' 'and dot_color dataframes have the same columns' + 'please check that the dot_size ' + 'and dot_color dataframes have the same columns' ) if standard_scale == 'group': @@ -672,7 +685,9 @@ def _dotplot( import matplotlib.colors - normalize = matplotlib.colors.Normalize(vmin=kwds.get('vmin'), vmax=kwds.get('vmax')) + normalize = matplotlib.colors.Normalize( + vmin=kwds.get('vmin'), vmax=kwds.get('vmax') + ) if color_on == 'square': if edge_color is None: @@ -723,7 +738,9 @@ def _dotplot( y_ticks = np.arange(dot_color.shape[0]) + 0.5 dot_ax.set_yticks(y_ticks) - dot_ax.set_yticklabels([dot_color.index[idx] for idx, _ in enumerate(y_ticks)], minor=False) + dot_ax.set_yticklabels( + [dot_color.index[idx] for idx, _ in enumerate(y_ticks)], minor=False + ) x_ticks = np.arange(dot_color.shape[1]) + 0.5 dot_ax.set_xticks(x_ticks) diff --git a/scanpy/plotting/_matrixplot.py b/scanpy/plotting/_matrixplot.py index 8af68f844d..d95678ba06 100644 --- a/scanpy/plotting/_matrixplot.py +++ b/scanpy/plotting/_matrixplot.py @@ -205,7 +205,9 @@ def _mainplot(self, ax): import matplotlib.colors - normalize = matplotlib.colors.Normalize(vmin=self.kwds.get('vmin'), vmax=self.kwds.get('vmax')) + normalize = matplotlib.colors.Normalize( + vmin=self.kwds.get('vmin'), vmax=self.kwds.get('vmax') + ) for axis in ['top', 'bottom', 'left', 'right']: ax.spines[axis].set_linewidth(1.5) diff --git a/scanpy/plotting/_preprocessing.py b/scanpy/plotting/_preprocessing.py index ad9e64d27b..9837773334 100644 --- a/scanpy/plotting/_preprocessing.py +++ b/scanpy/plotting/_preprocessing.py @@ -120,4 +120,6 @@ def filter_genes_dispersion( A string is appended to the default filename. Infer the filetype if ending on {{`'.pdf'`, `'.png'`, `'.svg'`}}. """ - highly_variable_genes(result, log=log, show=show, save=save, highly_variable_genes=False) + highly_variable_genes( + result, log=log, show=show, save=save, highly_variable_genes=False + ) diff --git a/scanpy/plotting/_qc.py b/scanpy/plotting/_qc.py index d33d5fec2a..3259e66425 100644 --- a/scanpy/plotting/_qc.py +++ b/scanpy/plotting/_qc.py @@ -75,8 +75,14 @@ def highest_expr_genes( mean_percent = norm_dict['X'].mean(axis=0) top_idx = np.argsort(mean_percent)[::-1][:n_top] counts_top_genes = norm_dict['X'][:, top_idx] - columns = adata.var_names[top_idx] if gene_symbols is None else adata.var[gene_symbols][top_idx] - counts_top_genes = pd.DataFrame(counts_top_genes, index=adata.obs_names, columns=columns) + columns = ( + adata.var_names[top_idx] + if gene_symbols is None + else adata.var[gene_symbols][top_idx] + ) + counts_top_genes = pd.DataFrame( + counts_top_genes, index=adata.obs_names, columns=columns + ) if not ax: # figsize is hardcoded to produce a tall image. To change the fig size, diff --git a/scanpy/plotting/_stacked_violin.py b/scanpy/plotting/_stacked_violin.py index 11a1f51efd..16abbaeeb9 100644 --- a/scanpy/plotting/_stacked_violin.py +++ b/scanpy/plotting/_stacked_violin.py @@ -308,7 +308,9 @@ def _mainplot(self, ax): _matrix = _matrix.iloc[:, self.var_names_idx_order] if self.categories_order is not None: - _matrix.index = _matrix.index.reorder_categories(self.categories_order, ordered=True) + _matrix.index = _matrix.index.reorder_categories( + self.categories_order, ordered=True + ) # get mean values for color and transform to color values # using colormap @@ -317,14 +319,18 @@ def _mainplot(self, ax): _color_df = _color_df.T import matplotlib.colors - norm = matplotlib.colors.Normalize(vmin=self.kwds.get('vmin'), vmax=self.kwds.get('vmax')) + norm = matplotlib.colors.Normalize( + vmin=self.kwds.get('vmin'), vmax=self.kwds.get('vmax') + ) cmap = pl.get_cmap(self.kwds.get('cmap', self.cmap)) if 'cmap' in self.kwds: del self.kwds['cmap'] colormap_array = cmap(norm(_color_df.values)) x_spacer_size = self.plot_x_padding y_spacer_size = self.plot_y_padding - self._make_rows_of_violinplots(ax, _matrix, colormap_array, _color_df, x_spacer_size, y_spacer_size) + self._make_rows_of_violinplots( + ax, _matrix, colormap_array, _color_df, x_spacer_size, y_spacer_size + ) # turn on axis for `ax` as this is turned off # by make_grid_spec when the axis is subdivided earlier. @@ -339,7 +345,9 @@ def _mainplot(self, ax): # 0.5 to position the ticks on the center of the violins y_ticks = np.arange(_color_df.shape[0]) + 0.5 ax.set_yticks(y_ticks) - ax.set_yticklabels([_color_df.index[idx] for idx, _ in enumerate(y_ticks)], minor=False) + ax.set_yticklabels( + [_color_df.index[idx] for idx, _ in enumerate(y_ticks)], minor=False + ) # 0.5 to position the ticks on the center of the violins x_ticks = np.arange(_color_df.shape[1]) + 0.5 @@ -354,7 +362,9 @@ def _mainplot(self, ax): return norm - def _make_rows_of_violinplots(self, ax, _matrix, colormap_array, _color_df, x_spacer_size, y_spacer_size): + def _make_rows_of_violinplots( + self, ax, _matrix, colormap_array, _color_df, x_spacer_size, y_spacer_size + ): import seaborn as sns # Slow import, only import if called row_palette = self.kwds.get('color', self.row_palette) @@ -393,8 +403,14 @@ def _make_rows_of_violinplots(self, ax, _matrix, colormap_array, _color_df, x_sp } ) ) - df['genes'] = df['genes'].astype('category').cat.reorder_categories(_matrix.columns) - df['categories'] = df['categories'].astype('category').cat.reorder_categories(_matrix.index.categories) + df['genes'] = ( + df['genes'].astype('category').cat.reorder_categories(_matrix.columns) + ) + df['categories'] = ( + df['categories'] + .astype('category') + .cat.reorder_categories(_matrix.index.categories) + ) # the ax need to be subdivided # define a layout of nrows = len(categories) rows @@ -496,7 +512,9 @@ def _setup_violin_axes_ticks(self, row_ax, num_cols): import matplotlib.ticker as ticker # use MaxNLocator to set 2 ticks - row_ax.yaxis.set_major_locator(ticker.MaxNLocator(nbins=2, steps=[1, 1.2, 10])) + row_ax.yaxis.set_major_locator( + ticker.MaxNLocator(nbins=2, steps=[1, 1.2, 10]) + ) yticks = row_ax.get_yticks() row_ax.set_yticks([yticks[0], yticks[-1]]) ticklabels = row_ax.get_yticklabels() @@ -513,7 +531,9 @@ def _setup_violin_axes_ticks(self, row_ax, num_cols): row_ax.set_xlabel('') row_ax.set_xticklabels([]) - row_ax.tick_params(axis='x', bottom=False, top=False, labeltop=False, labelbottom=False) + row_ax.tick_params( + axis='x', bottom=False, top=False, labeltop=False, labelbottom=False + ) @_doc_params( diff --git a/scanpy/plotting/_tools/__init__.py b/scanpy/plotting/_tools/__init__.py index 47e5537b69..e12eadc5d6 100644 --- a/scanpy/plotting/_tools/__init__.py +++ b/scanpy/plotting/_tools/__init__.py @@ -393,10 +393,14 @@ def _rank_genes_groups_plot( if min_logfoldchange is not None: df = rank_genes_groups_df(adata, group, key=key) # select genes with given log_fold change - genes_list = df[df.logfoldchanges > min_logfoldchange].names.tolist()[:n_genes] + genes_list = df[df.logfoldchanges > min_logfoldchange].names.tolist()[ + :n_genes + ] else: # get all genes that are 'non-nan' - genes_list = [gene for gene in adata.uns[key]['names'][group] if not pd.isnull(gene)][:n_genes] + genes_list = [ + gene for gene in adata.uns[key]['names'][group] if not pd.isnull(gene) + ][:n_genes] if len(genes_list) == 0: logg.warning(f'No genes found for group {group}') @@ -437,7 +441,9 @@ def _rank_genes_groups_plot( elif plot_type == 'matrixplot': from .._matrixplot import matrixplot - _pl = matrixplot(adata, var_names, groupby, values_df=values_df, return_fig=True, **kwds) + _pl = matrixplot( + adata, var_names, groupby, values_df=values_df, return_fig=True, **kwds + ) if title is not None and 'colorbar_title' not in kwds: _pl.legend(title=title) @@ -948,7 +954,11 @@ def rank_genes_groups_violin( _ax.legend_.remove() _ax.set_ylabel('expression') _ax.set_xticklabels(new_gene_names, rotation='vertical') - writekey = f"rank_genes_groups_" f"{adata.uns[key]['params']['groupby']}_" f"{group_name}" + writekey = ( + f"rank_genes_groups_" + f"{adata.uns[key]['params']['groupby']}_" + f"{group_name}" + ) savefig_or_show(writekey, show=show, save=save) axs.append(_ax) if not show: @@ -1137,11 +1147,15 @@ def embedding_density( if f'X_{basis}' not in adata.obsm_keys(): raise ValueError( - f'Cannot find the embedded representation `adata.obsm[X_{basis!r}]`. ' 'Compute the embedding first.' + f'Cannot find the embedded representation `adata.obsm[X_{basis!r}]`. ' + 'Compute the embedding first.' ) if key not in adata.obs or f'{key}_params' not in adata.uns: - raise ValueError('Please run `sc.tl.embedding_density()` first ' 'and specify the correct key.') + raise ValueError( + 'Please run `sc.tl.embedding_density()` first ' + 'and specify the correct key.' + ) if 'components' in kwargs: logg.warning( @@ -1164,11 +1178,15 @@ def embedding_density( if group is None and groupby is not None: raise ValueError( - 'Densities were calculated over an `.obs` covariate. ' 'Please specify a group from this covariate to plot.' + 'Densities were calculated over an `.obs` covariate. ' + 'Please specify a group from this covariate to plot.' ) if group is not None and groupby is None: - logg.warning("value of 'group' is ignored because densities " "were not calculated for an `.obs` covariate.") + logg.warning( + "value of 'group' is ignored because densities " + "were not calculated for an `.obs` covariate." + ) group = None if np.min(adata.obs[key]) < 0 or np.max(adata.obs[key]) > 1: @@ -1196,7 +1214,11 @@ def embedding_density( # if group is set, then plot it using multiple panels # (even if only one group is set) - if group is not None and not isinstance(group, str) and isinstance(group, cabc.Sequence): + if ( + group is not None + and not isinstance(group, str) + and isinstance(group, cabc.Sequence) + ): if ax is not None: raise ValueError("Can only specify `ax` if no `group` sequence is given.") fig, gs = _panel_grid(hspace, wspace, ncols, len(group)) @@ -1352,7 +1374,9 @@ def _get_values_to_plot( column = values_to_plot.replace('log10_', '') else: column = values_to_plot - values_df = pd.pivot(values_df, index='names', columns='group', values=column).fillna(1) + values_df = pd.pivot( + values_df, index='names', columns='group', values=column + ).fillna(1) if values_to_plot in ['log10_pvals', 'log10_pvals_adj']: values_df = -1 * np.log10(values_df) diff --git a/scanpy/plotting/_tools/paga.py b/scanpy/plotting/_tools/paga.py index 7aa9352932..99d6f4db78 100644 --- a/scanpy/plotting/_tools/paga.py +++ b/scanpy/plotting/_tools/paga.py @@ -205,19 +205,26 @@ def _compute_pos( iterations = layout_kwds['iterations'] else: iterations = 500 - pos_list = forceatlas2.forceatlas2(adjacency_solid, pos=init_coords, iterations=iterations) + pos_list = forceatlas2.forceatlas2( + adjacency_solid, pos=init_coords, iterations=iterations + ) pos = {n: [p[0], -p[1]] for n, p in enumerate(pos_list)} elif layout == 'eq_tree': nx_g_tree = nx.Graph(adj_tree) pos = _utils.hierarchy_pos(nx_g_tree, root) if len(pos) < adjacency_solid.shape[0]: - raise ValueError('This is a forest and not a single tree. ' 'Try another `layout`, e.g., {\'fr\'}.') + raise ValueError( + 'This is a forest and not a single tree. ' + 'Try another `layout`, e.g., {\'fr\'}.' + ) else: # igraph layouts g = _sc_utils.get_igraph_from_adjacency(adjacency_solid) if 'rt' in layout: g_tree = _sc_utils.get_igraph_from_adjacency(adj_tree) - pos_list = g_tree.layout(layout, root=root if isinstance(root, list) else [root]).coords + pos_list = g_tree.layout( + layout, root=root if isinstance(root, list) else [root] + ).coords elif layout == 'circle': pos_list = g.layout(layout).coords else: @@ -232,7 +239,9 @@ def _compute_pos( init_pos[:, 1] *= -1 init_coords = init_pos.tolist() try: - pos_list = g.layout(layout, seed=init_coords, weights='weight', **layout_kwds).coords + pos_list = g.layout( + layout, seed=init_coords, weights='weight', **layout_kwds + ).coords except AttributeError: # hack for empty graphs... pos_list = g.layout(layout, seed=init_coords, **layout_kwds).coords pos = {n: [p[0], -p[1]] for n, p in enumerate(pos_list)} @@ -431,12 +440,18 @@ def paga( groups_key = adata.uns['paga']['groups'] def is_flat(x): - has_one_per_category = isinstance(x, cabc.Collection) and len(x) == len(adata.obs[groups_key].cat.categories) + has_one_per_category = isinstance(x, cabc.Collection) and len(x) == len( + adata.obs[groups_key].cat.categories + ) return has_one_per_category or x is None or isinstance(x, str) - if isinstance(colors, cabc.Mapping) and isinstance(colors[next(iter(colors))], cabc.Mapping): + if isinstance(colors, cabc.Mapping) and isinstance( + colors[next(iter(colors))], cabc.Mapping + ): # handle paga pie, remap string keys to integers - names_to_ixs = {n: i for i, n in enumerate(adata.obs[groups_key].cat.categories)} + names_to_ixs = { + n: i for i, n in enumerate(adata.obs[groups_key].cat.categories) + } colors = {names_to_ixs.get(n, n): v for n, v in colors.items()} if is_flat(colors): colors = [colors] @@ -457,14 +472,21 @@ def is_flat(x): if colorbar is None: var_names = adata.var_names if adata.raw is None else adata.raw.var_names colorbars = [ - ((c in adata.obs_keys() and adata.obs[c].dtype.name != 'category') or (c in var_names)) for c in colors + ( + (c in adata.obs_keys() and adata.obs[c].dtype.name != 'category') + or (c in var_names) + ) + for c in colors ] else: colorbars = [False for _ in colors] if isinstance(root, str): if root not in labels: - raise ValueError('If `root` is a string, ' f'it needs to be one of {labels} not {root!r}.') + raise ValueError( + 'If `root` is a string, ' + f'it needs to be one of {labels} not {root!r}.' + ) root = list(labels).index(root) if isinstance(root, cabc.Sequence) and root[0] in labels: root = [list(labels).index(r) for r in root] @@ -608,7 +630,11 @@ def _paga_graph( import networkx as nx node_labels = labels # rename for clarity - if node_labels is not None and isinstance(node_labels, str) and node_labels != adata.uns['paga']['groups']: + if ( + node_labels is not None + and isinstance(node_labels, str) + and node_labels != adata.uns['paga']['groups'] + ): raise ValueError( 'Provide a list of group labels for the PAGA groups {}, not {}.'.format( adata.uns['paga']['groups'], node_labels @@ -619,9 +645,9 @@ def _paga_graph( node_labels = adata.obs[groups_key].cat.categories if (colors is None or colors == groups_key) and groups_key is not None: - if groups_key + '_colors' not in adata.uns or len(adata.obs[groups_key].cat.categories) != len( - adata.uns[groups_key + '_colors'] - ): + if groups_key + '_colors' not in adata.uns or len( + adata.obs[groups_key].cat.categories + ) != len(adata.uns[groups_key + '_colors']): _utils.add_colors_for_categorical_sample_annotation(adata, groups_key) colors = adata.uns[groups_key + '_colors'] for iname, name in enumerate(adata.obs[groups_key].cat.categories): @@ -687,7 +713,11 @@ def _paga_graph( colors = x_color # plot continuous annotation - if isinstance(colors, str) and colors in adata.obs and not is_categorical_dtype(adata.obs[colors]): + if ( + isinstance(colors, str) + and colors in adata.obs + and not is_categorical_dtype(adata.obs[colors]) + ): x_color = [] cats = adata.obs[groups_key].cat.categories for icat, cat in enumerate(cats): @@ -696,7 +726,11 @@ def _paga_graph( colors = x_color # plot categorical annotation - if isinstance(colors, str) and colors in adata.obs and is_categorical_dtype(adata.obs[colors]): + if ( + isinstance(colors, str) + and colors in adata.obs + and is_categorical_dtype(adata.obs[colors]) + ): asso_names, asso_matrix = _sc_utils.compute_association_matrix_of_groups( adata, prediction=groups_key, @@ -704,11 +738,16 @@ def _paga_graph( normalization='reference' if normalize_to_color else 'prediction', ) _utils.add_colors_for_categorical_sample_annotation(adata, colors) - asso_colors = _sc_utils.get_associated_colors_of_groups(adata.uns[colors + '_colors'], asso_matrix) + asso_colors = _sc_utils.get_associated_colors_of_groups( + adata.uns[colors + '_colors'], asso_matrix + ) colors = asso_colors if len(colors) != len(node_labels): - raise ValueError(f'Expected `colors` to be of length `{len(node_labels)}`, ' f'found `{len(colors)}`.') + raise ValueError( + f'Expected `colors` to be of length `{len(node_labels)}`, ' + f'found `{len(colors)}`.' + ) # count number of connected components n_components, labels = scipy.sparse.csgraph.connected_components(adjacency_solid) @@ -724,8 +763,13 @@ def _paga_graph( adjacency_solid = adjacency_solid.tocsc()[:, labels == largest_component] colors = np.array(colors)[labels == largest_component] node_labels = np.array(node_labels)[labels == largest_component] - cats_dropped = adata.obs[groups_key].cat.categories[labels != largest_component].tolist() - logg.info('Restricting graph to largest connected component by dropping categories\n' f'{cats_dropped}') + cats_dropped = ( + adata.obs[groups_key].cat.categories[labels != largest_component].tolist() + ) + logg.info( + 'Restricting graph to largest connected component by dropping categories\n' + f'{cats_dropped}' + ) nx_g_solid = nx.Graph(adjacency_solid) if dashed_edges is not None: raise ValueError('`single_component` only if `dashed_edges` is `None`.') @@ -757,7 +801,9 @@ def _paga_graph( widths = np.clip(widths, min_edge_width, max_edge_width) with warnings.catch_warnings(): warnings.simplefilter("ignore") - nx.draw_networkx_edges(nx_g_solid, pos, ax=ax, width=widths, edge_color='black') + nx.draw_networkx_edges( + nx_g_solid, pos, ax=ax, width=widths, edge_color='black' + ) # draw directed edges else: adjacency_transitions = adata.uns['paga'][transitions].copy() @@ -770,7 +816,9 @@ def _paga_graph( widths = base_edge_width * np.array(widths) if min_edge_width is not None or max_edge_width is not None: widths = np.clip(widths, min_edge_width, max_edge_width) - nx.draw_networkx_edges(g_dir, pos, ax=ax, width=widths, edge_color='black', arrowsize=arrowsize) + nx.draw_networkx_edges( + g_dir, pos, ax=ax, width=widths, edge_color='black', arrowsize=arrowsize + ) if export_to_gexf: if isinstance(colors[0], tuple): @@ -802,15 +850,21 @@ def _paga_graph( else: groups_sizes = np.ones(len(node_labels)) base_scale_scatter = 2000 - base_pie_size = base_scale_scatter / (np.sqrt(adjacency_solid.shape[0]) + 10) * node_size_scale + base_pie_size = ( + base_scale_scatter / (np.sqrt(adjacency_solid.shape[0]) + 10) * node_size_scale + ) median_group_size = np.median(groups_sizes) - groups_sizes = base_pie_size * np.power(groups_sizes / median_group_size, node_size_power) + groups_sizes = base_pie_size * np.power( + groups_sizes / median_group_size, node_size_power + ) if fontsize is None: fontsize = rcParams['legend.fontsize'] if fontoutline is not None: text_kwds = dict(text_kwds) - text_kwds['path_effects'] = [patheffects.withStroke(linewidth=fontoutline, foreground='w')] + text_kwds['path_effects'] = [ + patheffects.withStroke(linewidth=fontoutline, foreground='w') + ] # usual scatter plot if not isinstance(colors[0], cabc.Mapping): n_groups = len(pos_array) @@ -838,7 +892,8 @@ def _paga_graph( for ix, (xx, yy) in enumerate(zip(pos_array[:, 0], pos_array[:, 1])): if not isinstance(colors[ix], cabc.Mapping): raise ValueError( - f'{colors[ix]} is neither a dict of valid ' 'matplotlib colors nor a valid matplotlib color.' + f'{colors[ix]} is neither a dict of valid ' + 'matplotlib colors nor a valid matplotlib color.' ) color_single = colors[ix].keys() fracs = [colors[ix][c] for c in color_single] @@ -849,7 +904,10 @@ def _paga_graph( color_single.append('grey') fracs.append(1 - sum(fracs)) elif not np.isclose(total, 1): - raise ValueError(f'Expected fractions for node `{ix}` to be ' f'close to 1, found `{total}`.') + raise ValueError( + f'Expected fractions for node `{ix}` to be ' + f'close to 1, found `{total}`.' + ) cumsum = np.cumsum(fracs) cumsum = cumsum / cumsum[-1] @@ -863,7 +921,9 @@ def _paga_graph( xy = np.column_stack([x, y]) s = np.abs(xy).max() - sct = ax.scatter([xx], [yy], marker=xy, s=s ** 2 * groups_sizes[ix], color=color) + sct = ax.scatter( + [xx], [yy], marker=xy, s=s ** 2 * groups_sizes[ix], color=color + ) if node_labels is not None: ax.text( @@ -887,7 +947,9 @@ def paga_path( use_raw: bool = True, annotations: Sequence[str] = ('dpt_pseudotime',), color_map: Union[str, Colormap, None] = None, - color_maps_annotations: Mapping[str, Union[str, Colormap]] = MappingProxyType(dict(dpt_pseudotime='Greys')), + color_maps_annotations: Mapping[str, Union[str, Colormap]] = MappingProxyType( + dict(dpt_pseudotime='Greys') + ), palette_groups: Optional[Sequence[str]] = None, n_avg: int = 1, groups_key: Optional[str] = None, @@ -970,13 +1032,17 @@ def paga_path( if groups_key is None: if 'groups' not in adata.uns['paga']: - raise KeyError('Pass the key of the grouping with which you ran PAGA, ' 'using the parameter `groups_key`.') + raise KeyError( + 'Pass the key of the grouping with which you ran PAGA, ' + 'using the parameter `groups_key`.' + ) groups_key = adata.uns['paga']['groups'] groups_names = adata.obs[groups_key].cat.categories if 'dpt_pseudotime' not in adata.obs.keys(): raise ValueError( - '`pl.paga_path` requires computation of a pseudotime `tl.dpt` ' 'for ordering at single-cell resolution' + '`pl.paga_path` requires computation of a pseudotime `tl.dpt` ' + 'for ordering at single-cell resolution' ) if palette_groups is None: @@ -1015,7 +1081,9 @@ def moving_average(a): for ikey, key in enumerate(keys): x = [] for igroup, group in enumerate(nodes_ints): - idcs = np.arange(adata.n_obs)[adata.obs[groups_key].values == nodes_strs[igroup]] + idcs = np.arange(adata.n_obs)[ + adata.obs[groups_key].values == nodes_strs[igroup] + ] if len(idcs) == 0: raise ValueError( 'Did not find data points that match ' @@ -1024,7 +1092,9 @@ def moving_average(a): 'actually contains what you expect.' ) idcs_group = np.argsort( - adata.obs['dpt_pseudotime'].values[adata.obs[groups_key].values == nodes_strs[igroup]] + adata.obs['dpt_pseudotime'].values[ + adata.obs[groups_key].values == nodes_strs[igroup] + ] ) idcs = idcs[idcs_group] if key in adata.obs_keys(): @@ -1150,7 +1220,9 @@ def moving_average(a): ) arr = np.array(anno_dict[anno])[None, :] if anno not in color_maps_annotations: - color_map_anno = 'Vega10' if is_categorical_dtype(adata.obs[anno]) else 'Greys' + color_map_anno = ( + 'Vega10' if is_categorical_dtype(adata.obs[anno]) else 'Greys' + ) else: color_map_anno = color_maps_annotations[anno] img = anno_axis.imshow( diff --git a/scanpy/plotting/_tools/scatterplots.py b/scanpy/plotting/_tools/scatterplots.py index 9722caa329..9cf42fb762 100644 --- a/scanpy/plotting/_tools/scatterplots.py +++ b/scanpy/plotting/_tools/scatterplots.py @@ -143,7 +143,8 @@ def embedding( use_raw = layer is None and adata.raw is not None if use_raw and layer is not None: raise ValueError( - "Cannot use both a layer and the raw representation. Was passed:" f"use_raw={use_raw}, layer={layer}." + "Cannot use both a layer and the raw representation. Was passed:" + f"use_raw={use_raw}, layer={layer}." ) if wspace is None: @@ -151,7 +152,10 @@ def embedding( # current figure size wspace = 0.75 / rcParams['figure.figsize'][0] + 0.02 if adata.raw is None and use_raw: - raise ValueError("`use_raw` is set to True but AnnData object does not have raw. " "Please check.") + raise ValueError( + "`use_raw` is set to True but AnnData object does not have raw. " + "Please check." + ) # turn color into a python list color = [color] if isinstance(color, str) or color is None else list(color) if title is not None: @@ -160,17 +164,24 @@ def embedding( # get the points position and the components list # (only if components is not None) - data_points, components_list = _get_data_points(adata, basis, projection, components, scale_factor) + data_points, components_list = _get_data_points( + adata, basis, projection, components, scale_factor + ) # Setup layout. # Most of the code is for the case when multiple plots are required # 'color' is a list of names that want to be plotted. # Eg. ['Gene1', 'louvain', 'Gene2']. # component_list is a list of components [[0,1], [1,2]] - if (not isinstance(color, str) and isinstance(color, cabc.Sequence) and len(color) > 1) or len(components_list) > 1: + if ( + not isinstance(color, str) + and isinstance(color, cabc.Sequence) + and len(color) > 1 + ) or len(components_list) > 1: if ax is not None: raise ValueError( - "Cannot specify `ax` when plotting multiple panels " "(each for a given value of 'color')." + "Cannot specify `ax` when plotting multiple panels " + "(each for a given value of 'color')." ) if len(components_list) == 0: components_list = [None] @@ -222,7 +233,9 @@ def embedding( # color=gene1, components=[1,2], color=gene1, components=[2,3], # color=gene2, components = [1, 2], color=gene2, components=[2,3], # ] - for count, (value_to_plot, component_idx) in enumerate(itertools.product(color, idx_components)): + for count, (value_to_plot, component_idx) in enumerate( + itertools.product(color, idx_components) + ): color_source_vector = _get_color_source_vector( adata, value_to_plot, @@ -280,7 +293,9 @@ def embedding( if categorical: kwargs['vmin'] = kwargs['vmax'] = None else: - kwargs['vmin'], kwargs['vmax'] = _get_vmin_vmax(vmin, vmax, count, color_vector) + kwargs['vmin'], kwargs['vmax'] = _get_vmin_vmax( + vmin, vmax, count, color_vector + ) # make the scatter plot if projection == '3d': @@ -389,7 +404,9 @@ def embedding( continue if legend_fontoutline is not None: - path_effect = [patheffects.withStroke(linewidth=legend_fontoutline, foreground='w')] + path_effect = [ + patheffects.withStroke(linewidth=legend_fontoutline, foreground='w') + ] else: path_effect = None @@ -543,11 +560,17 @@ def _wraps_plot_scatter(wrapper): wrapper_params.pop("adata") params.update(wrapper_params) - annotations = {k: v.annotation for k, v in params.items() if v.annotation != inspect.Parameter.empty} + annotations = { + k: v.annotation + for k, v in params.items() + if v.annotation != inspect.Parameter.empty + } if wrapper_sig.return_annotation is not inspect.Signature.empty: annotations["return"] = wrapper_sig.return_annotation - wrapper.__signature__ = inspect.Signature(list(params.values()), return_annotation=wrapper_sig.return_annotation) + wrapper.__signature__ = inspect.Signature( + list(params.values()), return_annotation=wrapper_sig.return_annotation + ) wrapper.__annotations__ = annotations return wrapper @@ -636,7 +659,9 @@ def diffmap(adata, **kwargs) -> Union[Axes, List[Axes], None]: scatter_bulk=doc_scatter_embedding, show_save_ax=doc_show_save_ax, ) -def draw_graph(adata: AnnData, *, layout: Optional[_IGraphLayout] = None, **kwargs) -> Union[Axes, List[Axes], None]: +def draw_graph( + adata: AnnData, *, layout: Optional[_IGraphLayout] = None, **kwargs +) -> Union[Axes, List[Axes], None]: """\ Scatter plot in graph-drawing basis. @@ -659,7 +684,9 @@ def draw_graph(adata: AnnData, *, layout: Optional[_IGraphLayout] = None, **kwar basis = 'draw_graph_' + layout if 'X_' + basis not in adata.obsm_keys(): raise ValueError( - 'Did not find {} in adata.obs. Did you compute layout {}?'.format('draw_graph_' + layout, layout) + 'Did not find {} in adata.obs. Did you compute layout {}?'.format( + 'draw_graph_' + layout, layout + ) ) return embedding(adata, basis, **kwargs) @@ -697,12 +724,15 @@ def pca( If `show==False` a :class:`~matplotlib.axes.Axes` or a list of it. """ if not annotate_var_explained: - return embedding(adata, 'pca', show=show, return_fig=return_fig, save=save, **kwargs) + return embedding( + adata, 'pca', show=show, return_fig=return_fig, save=save, **kwargs + ) else: if 'pca' not in adata.obsm.keys() and 'X_pca' not in adata.obsm.keys(): raise KeyError( - f"Could not find entry in `obsm` for 'pca'.\n" f"Available keys are: {list(adata.obsm.keys())}." + f"Could not find entry in `obsm` for 'pca'.\n" + f"Available keys are: {list(adata.obsm.keys())}." ) label_dict = { @@ -814,7 +844,9 @@ def spatial( library_id, spatial_data = _check_spatial_data(adata.uns, library_id) img, img_key = _check_img(spatial_data, img, img_key, bw=bw) spot_size = _check_spot_size(spatial_data, spot_size) - scale_factor = _check_scale_factor(spatial_data, img_key=img_key, scale_factor=scale_factor) + scale_factor = _check_scale_factor( + spatial_data, img_key=img_key, scale_factor=scale_factor + ) crop_coord = _check_crop_coord(crop_coord, scale_factor) na_color = _check_na_color(na_color, img=img) @@ -881,7 +913,8 @@ def _get_data_points( basis_key = f"X_{basis}" else: raise KeyError( - f"Could not find entry in `obsm` for '{basis}'.\n" f"Available keys are: {list(adata.obsm.keys())}." + f"Could not find entry in `obsm` for '{basis}'.\n" + f"Available keys are: {list(adata.obsm.keys())}." ) n_dims = 2 @@ -901,7 +934,9 @@ def _get_data_points( r_value = 3 if projection == '3d' else 2 _components_list = np.arange(adata.obsm[basis_key].shape[1]) + 1 - components = [",".join(map(str, x)) for x in combinations(_components_list, r=r_value)] + components = [ + ",".join(map(str, x)) for x in combinations(_components_list, r=r_value) + ] components_list = [] offset = 0 @@ -913,7 +948,9 @@ def _get_data_points( if isinstance(components, str): # eg: components='1,2' - components_list.append(tuple(int(x.strip()) - 1 + offset for x in components.split(','))) + components_list.append( + tuple(int(x.strip()) - 1 + offset for x in components.split(',')) + ) elif isinstance(components, cabc.Sequence): if isinstance(components[0], int): @@ -925,11 +962,14 @@ def _get_data_points( # More than one component can be given and is stored # as a new item of components_list for comp in components: - components_list.append(tuple(int(x.strip()) - 1 + offset for x in comp.split(','))) + components_list.append( + tuple(int(x.strip()) - 1 + offset for x in comp.split(',')) + ) else: raise ValueError( - "Given components: '{}' are not valid. Please check. " "A valid example is `components='2,3'`" + "Given components: '{}' are not valid. Please check. " + "A valid example is `components='2,3'`" ) # check if the components are present in the data try: @@ -938,13 +978,16 @@ def _get_data_points( data_points.append(adata.obsm[basis_key][:, comp]) except Exception: raise ValueError( - "Given components: '{}' are not valid. Please check. " "A valid example is `components='2,3'`" + "Given components: '{}' are not valid. Please check. " + "A valid example is `components='2,3'`" ) if basis == 'diffmap': # remove the offset added in the case of diffmap, such that # plot_scatter can print the labels correctly. - components_list = [tuple(number - 1 for number in comp) for comp in components_list] + components_list = [ + tuple(number - 1 for number in comp) for comp in components_list + ] else: data_points = [np.array(adata.obsm[basis_key])[:, offset : offset + n_dims]] components_list = [] @@ -971,7 +1014,9 @@ def _add_categorical_legend( """Add a legend to the passed Axes.""" if na_in_legend and pd.isnull(color_source_vector).any(): if "NA" in color_source_vector: - raise NotImplementedError("No fallback for null labels has been defined if NA already in categories.") + raise NotImplementedError( + "No fallback for null labels has been defined if NA already in categories." + ) color_source_vector = color_source_vector.add_categories("NA").fillna("NA") palette = palette.copy() palette["NA"] = na_color @@ -995,7 +1040,11 @@ def _add_categorical_legend( ) elif legend_loc == 'on data': # identify centroids to put labels - all_pos = pd.DataFrame(scatter_array, columns=["x", "y"]).groupby(color_source_vector, observed=True).median() + all_pos = ( + pd.DataFrame(scatter_array, columns=["x", "y"]) + .groupby(color_source_vector, observed=True) + .median() + ) for label, x_pos, y_pos in all_pos.itertuples(): ax.text( @@ -1013,7 +1062,9 @@ def _add_categorical_legend( _utils._tmp_cluster_pos = all_pos.values -def _get_color_source_vector(adata, value_to_plot, use_raw=False, gene_symbols=None, layer=None, groups=None): +def _get_color_source_vector( + adata, value_to_plot, use_raw=False, gene_symbols=None, layer=None, groups=None +): """ Get array from adata that colors will be based on. """ @@ -1023,7 +1074,11 @@ def _get_color_source_vector(adata, value_to_plot, use_raw=False, gene_symbols=N # _color_vector handles this. # https://github.com/matplotlib/matplotlib/issues/18294 return np.broadcast_to(np.nan, adata.n_obs) - if gene_symbols is not None and value_to_plot not in adata.obs.columns and value_to_plot not in adata.var_names: + if ( + gene_symbols is not None + and value_to_plot not in adata.obs.columns + and value_to_plot not in adata.var_names + ): # We should probably just make an index for this, and share it over runs value_to_plot = adata.var.index[adata.var[gene_symbols] == value_to_plot][ 0 @@ -1042,7 +1097,9 @@ def _get_palette(adata, values_key: str, palette=None): values = pd.Categorical(adata.obs[values_key]) if palette: _utils._set_colors_for_categorical_obs(adata, values_key, palette) - elif color_key not in adata.uns or len(adata.uns[color_key]) < len(values.categories): + elif color_key not in adata.uns or len(adata.uns[color_key]) < len( + values.categories + ): # set a default palette in case that no colors or few colors are found _utils._set_default_colors_for_categorical_obs(adata, values_key) else: @@ -1050,7 +1107,9 @@ def _get_palette(adata, values_key: str, palette=None): return dict(zip(values.categories, adata.uns[color_key])) -def _color_vector(adata, values_key: str, values, palette, na_color="lightgray") -> Tuple[np.ndarray, bool]: +def _color_vector( + adata, values_key: str, values, palette, na_color="lightgray" +) -> Tuple[np.ndarray, bool]: """ Map array of values to array of hex (plus alpha) codes. @@ -1069,7 +1128,10 @@ def _color_vector(adata, values_key: str, values, palette, na_color="lightgray") if not is_categorical_dtype(values): return values, False else: # is_categorical_dtype(values) - color_map = {k: to_hex(v) for k, v in _get_palette(adata, values_key, palette=palette).items()} + color_map = { + k: to_hex(v) + for k, v in _get_palette(adata, values_key, palette=palette).items() + } # If color_map does not have unique values, this can be slow as the # result is not categorical color_vector = values.map(color_map) @@ -1102,14 +1164,19 @@ def _basis2name(basis): return component_name -def _check_spot_size(spatial_data: Optional[Mapping], spot_size: Optional[float]) -> float: +def _check_spot_size( + spatial_data: Optional[Mapping], spot_size: Optional[float] +) -> float: """ Resolve spot_size value. This is a required argument for spatial plots. """ if spatial_data is None and spot_size is None: - raise ValueError("When .uns['spatial'][library_id] does not exist, spot_size must be " "provided directly.") + raise ValueError( + "When .uns['spatial'][library_id] does not exist, spot_size must be " + "provided directly." + ) elif spot_size is None: return spatial_data['scalefactors']['spot_diameter_fullres'] else: @@ -1130,7 +1197,9 @@ def _check_scale_factor( return 1.0 -def _check_spatial_data(uns: Mapping, library_id: Union[Empty, None, str]) -> Tuple[Optional[str], Optional[Mapping]]: +def _check_spatial_data( + uns: Mapping, library_id: Union[Empty, None, str] +) -> Tuple[Optional[str], Optional[Mapping]]: """ Given a mapping, try and extract a library id/ mapping with spatial data. @@ -1187,7 +1256,9 @@ def _check_crop_coord( return crop_coord -def _check_na_color(na_color: Optional[ColorLike], *, img: Optional[np.ndarray] = None) -> ColorLike: +def _check_na_color( + na_color: Optional[ColorLike], *, img: Optional[np.ndarray] = None +) -> ColorLike: if na_color is None: if img is not None: na_color = (0.0, 0.0, 0.0, 0.0) diff --git a/scanpy/plotting/_utils.py b/scanpy/plotting/_utils.py index 214019755f..070214a572 100644 --- a/scanpy/plotting/_utils.py +++ b/scanpy/plotting/_utils.py @@ -28,7 +28,9 @@ ColorLike = Union[str, Tuple[float, ...]] _IGraphLayout = Literal['fa', 'fr', 'rt', 'rt_circular', 'drl', 'eq_tree', ...] _FontWeight = Literal['light', 'normal', 'medium', 'semibold', 'bold', 'heavy', 'black'] -_FontSize = Literal['xx-small', 'x-small', 'small', 'medium', 'large', 'x-large', 'xx-large'] +_FontSize = Literal[ + 'xx-small', 'x-small', 'small', 'medium', 'large', 'x-large', 'xx-large' +] class _AxesSubplot(Axes, axes.SubplotBase, ABC): @@ -67,7 +69,9 @@ def matrix( ax.set_xticks(range(len(xticks)), xticks, rotation='vertical') if yticks is not None: ax.set_yticks(range(len(yticks)), yticks) - pl.colorbar(img, shrink=colorbar_shrink, ax=ax) # need a figure instance for colorbar + pl.colorbar( + img, shrink=colorbar_shrink, ax=ax + ) # need a figure instance for colorbar savefig_or_show('matrix', show=show, save=save) @@ -152,7 +156,9 @@ def timeseries_subplot( ax.legend(frameon=False) -def timeseries_as_heatmap(X: np.ndarray, var_names: Collection[str] = (), highlights_x=(), color_map=None): +def timeseries_as_heatmap( + X: np.ndarray, var_names: Collection[str] = (), highlights_x=(), color_map=None +): """\ Plot timeseries as heatmap. @@ -261,7 +267,10 @@ def savefig(writekey, dpi=None, ext=None): """ if dpi is None: # we need this as in notebooks, the internal figures are also influenced by 'savefig.dpi' this... - if not isinstance(rcParams['savefig.dpi'], str) and rcParams['savefig.dpi'] < 150: + if ( + not isinstance(rcParams['savefig.dpi'], str) + and rcParams['savefig.dpi'] < 150 + ): if settings._low_resolution_warning: logg.warning( 'You are using a low resolution (dpi<150) for saving figures.\n' @@ -349,7 +358,9 @@ def _validate_palette(adata, key): adata.uns[color_key] = _palette -def _set_colors_for_categorical_obs(adata, value_to_plot, palette: Union[str, Sequence[str], Cycler]): +def _set_colors_for_categorical_obs( + adata, value_to_plot, palette: Union[str, Sequence[str], Cycler] +): """ Sets the adata.uns[value_to_plot + '_colors'] according to the given palette @@ -398,7 +409,10 @@ def _set_colors_for_categorical_obs(adata, value_to_plot, palette: Union[str, Se if color in additional_colors: color = additional_colors[color] else: - raise ValueError("The following color value of the given palette " f"is not valid: {color}") + raise ValueError( + "The following color value of the given palette " + f"is not valid: {color}" + ) _color_list.append(color) palette = cycler(color=_color_list) @@ -458,7 +472,9 @@ def _set_default_colors_for_categorical_obs(adata, value_to_plot): adata.uns[value_to_plot + '_colors'] = palette[:length] -def add_colors_for_categorical_sample_annotation(adata, key, palette=None, force_update_colors=False): +def add_colors_for_categorical_sample_annotation( + adata, key, palette=None, force_update_colors=False +): color_key = f"{key}_colors" colors_needed = len(adata.obs[key].cat.categories) @@ -501,10 +517,14 @@ def plot_edges(axs, adata, basis, edges_width, edges_color, neighbors_key=None): def plot_arrows(axs, adata, basis, arrows_kwds=None): if not isinstance(axs, cabc.Sequence): axs = [axs] - v_prefix = next((p for p in ['velocity', 'Delta'] if f'{p}_{basis}' in adata.obsm), None) + v_prefix = next( + (p for p in ['velocity', 'Delta'] if f'{p}_{basis}' in adata.obsm), None + ) if v_prefix is None: raise ValueError( - "`arrows=True` requires " f"`'velocity_{basis}'` from scvelo or " f"`'Delta_{basis}'` from velocyto." + "`arrows=True` requires " + f"`'velocity_{basis}'` from scvelo or " + f"`'Delta_{basis}'` from velocyto." ) if v_prefix == 'velocity': logg.warning( @@ -586,7 +606,9 @@ def setup_axes( if show_ticks: base_width *= 1.1 - draw_region_width = base_width - left_offset - top_offset - 0.5 # this is kept constant throughout + draw_region_width = ( + base_width - left_offset - top_offset - 0.5 + ) # this is kept constant throughout right_margin_factor = sum([1 + right_margin for right_margin in right_margin_list]) width_without_offsets = ( @@ -607,7 +629,9 @@ def setup_axes( left_positions = [left_offset_frac, left_offset_frac + draw_region_width_frac] for i in range(1, len(panels)): right_margin = right_margin_list[i - 1] - left_positions.append(left_positions[-1] + right_margin * draw_region_width_frac) + left_positions.append( + left_positions[-1] + right_margin * draw_region_width_frac + ) left_positions.append(left_positions[-1] + draw_region_width_frac) panel_pos = [[bottom_offset], [1 - top_offset], left_positions] @@ -711,7 +735,10 @@ def scatter_base( ) if colorbars[icolor]: width = 0.006 * draw_region_width / len(colors) - left = panel_pos[2][2 * icolor + 1] + (1.2 if projection == '3d' else 0.2) * width + left = ( + panel_pos[2][2 * icolor + 1] + + (1.2 if projection == '3d' else 0.2) * width + ) rectangle = [left, bottom, width, height] fig = pl.gcf() ax_cb = fig.add_axes(rectangle) @@ -734,7 +761,11 @@ def scatter_base( s=10, zorder=20, ) - highlight_text = highlights_labels[iihighlight] if len(highlights_labels) > 0 else str(ihighlight) + highlight_text = ( + highlights_labels[iihighlight] + if len(highlights_labels) > 0 + else str(ihighlight) + ) # the following is a Python 2 compatibility hack ax.text( *([d[0] for d in data] + [highlight_text]), @@ -749,7 +780,10 @@ def scatter_base( ax.set_zticks([]) # set default axis_labels if axis_labels is None: - axis_labels = [[component_name + str(i) for i in component_indexnames] for _ in range(len(axs))] + axis_labels = [ + [component_name + str(i) for i in component_indexnames] + for _ in range(len(axs)) + ] else: axis_labels = [axis_labels for _ in range(len(axs))] for iax, ax in enumerate(axs): @@ -937,7 +971,9 @@ def zoom(ax, xy='x', factor=1): ---------- """ limits = ax.get_xlim() if xy == 'x' else ax.get_ylim() - new_limits = 0.5 * (limits[0] + limits[1]) + 1.0 / factor * np.array((-0.5, 0.5)) * (limits[1] - limits[0]) + new_limits = 0.5 * (limits[0] + limits[1]) + 1.0 / factor * np.array( + (-0.5, 0.5) + ) * (limits[1] - limits[0]) if xy == 'x': ax.set_xlim(new_limits) else: @@ -1019,7 +1055,9 @@ def check_projection(projection): mpl_version = parse(mpl.__version__) if mpl_version < parse("3.3.3"): - raise ImportError(f"3d plotting requires matplotlib > 3.3.3. Found {mpl.__version__}") + raise ImportError( + f"3d plotting requires matplotlib > 3.3.3. Found {mpl.__version__}" + ) def circles(x, y, s, ax, marker=None, c='b', vmin=None, vmax=None, **kwargs): diff --git a/scanpy/plotting/palettes.py b/scanpy/plotting/palettes.py index f173719049..3ea963de99 100644 --- a/scanpy/plotting/palettes.py +++ b/scanpy/plotting/palettes.py @@ -210,4 +210,6 @@ def _plot_color_cycle(clists: Mapping[str, Sequence[str]]): if __name__ == '__main__': - _plot_color_cycle({name: colors for name, colors in globals().items() if isinstance(colors, list)}) + _plot_color_cycle( + {name: colors for name, colors in globals().items() if isinstance(colors, list)} + ) diff --git a/scanpy/preprocessing/_combat.py b/scanpy/preprocessing/_combat.py index ee5761798c..e2d8140bca 100644 --- a/scanpy/preprocessing/_combat.py +++ b/scanpy/preprocessing/_combat.py @@ -10,7 +10,9 @@ from .._utils import sanitize_anndata -def _design_matrix(model: pd.DataFrame, batch_key: str, batch_levels: Collection[str]) -> pd.DataFrame: +def _design_matrix( + model: pd.DataFrame, batch_key: str, batch_levels: Collection[str] +) -> pd.DataFrame: """\ Computes a simple design matrix. @@ -42,7 +44,9 @@ def _design_matrix(model: pd.DataFrame, batch_key: str, batch_levels: Collection if other_cols: col_repr = " + ".join("Q('{}')".format(x) for x in other_cols) - factor_matrix = patsy.dmatrix("~ 0 + {}".format(col_repr), model[other_cols], return_type="dataframe") + factor_matrix = patsy.dmatrix( + "~ 0 + {}".format(col_repr), model[other_cols], return_type="dataframe" + ) design = pd.concat((design, factor_matrix), axis=1) logg.info(f"Found {len(other_cols)} categorical variables:") @@ -105,7 +109,9 @@ def _standardize_data( # Compute the means if np.sum(var_pooled == 0) > 0: print(f'Found {np.sum(var_pooled == 0)} genes with zero variance.') - stand_mean = np.dot(grand_mean.T.reshape((len(grand_mean), 1)), np.ones((1, int(n_array)))) + stand_mean = np.dot( + grand_mean.T.reshape((len(grand_mean), 1)), np.ones((1, int(n_array))) + ) tmp = np.array(design.copy()) tmp[:, :n_batch] = 0 stand_mean += np.dot(tmp, B_hat).T @@ -169,7 +175,9 @@ def combat( cov_exist = np.isin(covariates, adata.obs_keys()) if np.any(~cov_exist): missing_cov = np.array(covariates)[~cov_exist].tolist() - raise ValueError('Could not find the covariate(s) {!r} in adata.obs'.format(missing_cov)) + raise ValueError( + 'Could not find the covariate(s) {!r} in adata.obs'.format(missing_cov) + ) if key in covariates: raise ValueError('Batch key and covariates cannot overlap') @@ -201,7 +209,9 @@ def combat( logg.info("Fitting L/S model and finding priors\n") batch_design = design[design.columns[:n_batch]] # first estimate of the additive batch effect - gamma_hat = (la.inv(batch_design.T @ batch_design) @ batch_design.T @ s_data.T).values + gamma_hat = ( + la.inv(batch_design.T @ batch_design) @ batch_design.T @ s_data.T + ).values delta_hat = [] # first estimate for the multiplicative batch effect @@ -250,7 +260,10 @@ def combat( dsq = np.sqrt(delta_star[j, :]) dsq = dsq.reshape((len(dsq), 1)) denom = np.dot(dsq, np.ones((1, n_batches[j]))) - numer = np.array(bayesdata.iloc[:, batch_idxs] - np.dot(batch_design.iloc[batch_idxs], gamma_star).T) + numer = np.array( + bayesdata.iloc[:, batch_idxs] + - np.dot(batch_design.iloc[batch_idxs], gamma_star).T + ) bayesdata.iloc[:, batch_idxs] = numer / denom vpsq = np.sqrt(var_pooled).reshape((len(var_pooled), 1)) @@ -316,12 +329,16 @@ def _it_sol( # in the loop, gamma and delta are updated together. they depend on each other. we iterate until convergence. while change > conv: g_new = (t2 * n * g_hat + d_old * g_bar) / (t2 * n + d_old) - sum2 = s_data - g_new.reshape((g_new.shape[0], 1)) @ np.ones((1, s_data.shape[1])) + sum2 = s_data - g_new.reshape((g_new.shape[0], 1)) @ np.ones( + (1, s_data.shape[1]) + ) sum2 = sum2 ** 2 sum2 = sum2.sum(axis=1) d_new = (0.5 * sum2 + b) / (n / 2.0 + a - 1.0) - change = max((abs(g_new - g_old) / g_old).max(), (abs(d_new - d_old) / d_old).max()) + change = max( + (abs(g_new - g_old) / g_old).max(), (abs(d_new - d_old) / d_old).max() + ) g_old = g_new # .copy() d_old = d_new # .copy() count = count + 1 diff --git a/scanpy/preprocessing/_deprecated/__init__.py b/scanpy/preprocessing/_deprecated/__init__.py index 0e768454c1..2bb8730540 100644 --- a/scanpy/preprocessing/_deprecated/__init__.py +++ b/scanpy/preprocessing/_deprecated/__init__.py @@ -36,9 +36,15 @@ def normalize_per_cell_weinreb16_deprecated( gene_subset = np.all(X <= counts_per_cell[:, None] * max_fraction, axis=0) if issparse(X): gene_subset = gene_subset.A1 - tc_include = X[:, gene_subset].sum(1).A1 if issparse(X) else X[:, gene_subset].sum(1) + tc_include = ( + X[:, gene_subset].sum(1).A1 if issparse(X) else X[:, gene_subset].sum(1) + ) - X_norm = X.multiply(csr_matrix(1 / tc_include[:, None])) if issparse(X) else X / tc_include[:, None] + X_norm = ( + X.multiply(csr_matrix(1 / tc_include[:, None])) + if issparse(X) + else X / tc_include[:, None] + ) if mult_with_mean: X_norm *= np.mean(counts_per_cell) diff --git a/scanpy/preprocessing/_deprecated/highly_variable_genes.py b/scanpy/preprocessing/_deprecated/highly_variable_genes.py index 86d68c3a77..1487b60feb 100644 --- a/scanpy/preprocessing/_deprecated/highly_variable_genes.py +++ b/scanpy/preprocessing/_deprecated/highly_variable_genes.py @@ -104,7 +104,9 @@ def filter_genes_dispersion( If a data matrix `X` is passed, the annotation is returned as `np.recarray` with the same information stored in fields: `gene_subset`, `means`, `dispersions`, `dispersion_norm`. """ - if n_top_genes is not None and not all(x is None for x in [min_disp, max_disp, min_mean, max_mean]): + if n_top_genes is not None and not all( + x is None for x in [min_disp, max_disp, min_mean, max_mean] + ): logg.info('If you pass `n_top_genes`, all cutoffs are ignored.') if min_disp is None: min_disp = 0.5 @@ -168,7 +170,10 @@ def filter_genes_dispersion( disp_mean_bin[one_gene_per_bin] = 0 # actually do the normalization df['dispersion_norm'] = ( - df['dispersion'].values - disp_mean_bin[df['mean_bin'].values].values # use values here as index differs + df['dispersion'].values + - disp_mean_bin[ + df['mean_bin'].values + ].values # use values here as index differs ) / disp_std_bin[df['mean_bin'].values].values elif flavor == 'cell_ranger': from statsmodels import robust @@ -184,7 +189,9 @@ def filter_genes_dispersion( warnings.simplefilter('ignore') disp_mad_bin = disp_grouped.apply(robust.mad) df['dispersion_norm'] = ( - np.abs(df['dispersion'].values - disp_median_bin[df['mean_bin'].values].values) + np.abs( + df['dispersion'].values - disp_median_bin[df['mean_bin'].values].values + ) / disp_mad_bin[df['mean_bin'].values].values ) else: @@ -192,10 +199,15 @@ def filter_genes_dispersion( dispersion_norm = df['dispersion_norm'].values.astype('float32') if n_top_genes is not None: dispersion_norm = dispersion_norm[~np.isnan(dispersion_norm)] - dispersion_norm[::-1].sort() # interestingly, np.argpartition is slightly slower + dispersion_norm[ + ::-1 + ].sort() # interestingly, np.argpartition is slightly slower disp_cut_off = dispersion_norm[n_top_genes - 1] gene_subset = df['dispersion_norm'].values >= disp_cut_off - logg.debug(f'the {n_top_genes} top genes correspond to a ' f'normalized dispersion cutoff of {disp_cut_off}') + logg.debug( + f'the {n_top_genes} top genes correspond to a ' + f'normalized dispersion cutoff of {disp_cut_off}' + ) else: max_disp = np.inf if max_disp is None else max_disp dispersion_norm[np.isnan(dispersion_norm)] = 0 # similar to Seurat diff --git a/scanpy/preprocessing/_highly_variable_genes.py b/scanpy/preprocessing/_highly_variable_genes.py index 89b01964e6..039a57cb58 100644 --- a/scanpy/preprocessing/_highly_variable_genes.py +++ b/scanpy/preprocessing/_highly_variable_genes.py @@ -52,7 +52,9 @@ def _highly_variable_genes_seurat_v3( try: from skmisc.loess import loess except ImportError: - raise ImportError('Please install skmisc package via `pip install --user scikit-misc') + raise ImportError( + 'Please install skmisc package via `pip install --user scikit-misc' + ) X = adata.layers[layer] if layer is not None else adata.X if check_values and (check_nonnegative_integers(X) == False): @@ -108,7 +110,9 @@ def _highly_variable_genes_seurat_v3( batch_counts_sum = batch_counts.sum(axis=0) norm_gene_var = (1 / ((N - 1) * np.square(reg_std))) * ( - (N * np.square(mean)) + squared_batch_counts_sum - 2 * batch_counts_sum * mean + (N * np.square(mean)) + + squared_batch_counts_sum + - 2 * batch_counts_sum * mean ) norm_gene_vars.append(norm_gene_var.reshape(1, -1)) @@ -118,7 +122,9 @@ def _highly_variable_genes_seurat_v3( # this is done in SelectIntegrationFeatures() in Seurat v3 ranked_norm_gene_vars = ranked_norm_gene_vars.astype(np.float32) - num_batches_high_var = np.sum((ranked_norm_gene_vars < n_top_genes).astype(int), axis=0) + num_batches_high_var = np.sum( + (ranked_norm_gene_vars < n_top_genes).astype(int), axis=0 + ) ranked_norm_gene_vars[ranked_norm_gene_vars >= n_top_genes] = np.nan ma_ranked = np.ma.masked_invalid(ranked_norm_gene_vars) median_ranked = np.ma.median(ma_ranked, axis=0).filled(np.nan) @@ -154,9 +160,13 @@ def _highly_variable_genes_seurat_v3( adata.var['highly_variable_rank'] = df['highly_variable_rank'].values adata.var['means'] = df['means'].values adata.var['variances'] = df['variances'].values - adata.var['variances_norm'] = df['variances_norm'].values.astype('float64', copy=False) + adata.var['variances_norm'] = df['variances_norm'].values.astype( + 'float64', copy=False + ) if batch_key is not None: - adata.var['highly_variable_nbatches'] = df['highly_variable_nbatches'].values + adata.var['highly_variable_nbatches'] = df[ + 'highly_variable_nbatches' + ].values if subset: adata._inplace_subset_var(df['highly_variable'].values) else: @@ -220,11 +230,16 @@ def _highly_variable_genes_single_batch( ) # Circumvent pandas 0.23 bug. Both sides of the assignment have dtype==float32, # but there’s still a dtype error without “.value”. - disp_std_bin[one_gene_per_bin.values] = disp_mean_bin[one_gene_per_bin.values].values + disp_std_bin[one_gene_per_bin.values] = disp_mean_bin[ + one_gene_per_bin.values + ].values disp_mean_bin[one_gene_per_bin.values] = 0 # actually do the normalization df['dispersions_norm'] = ( - df['dispersions'].values - disp_mean_bin[df['mean_bin'].values].values # use values here as index differs + df['dispersions'].values + - disp_mean_bin[ + df['mean_bin'].values + ].values # use values here as index differs ) / disp_std_bin[df['mean_bin'].values].values elif flavor == 'cell_ranger': from statsmodels import robust @@ -247,13 +262,18 @@ def _highly_variable_genes_single_batch( dispersion_norm = df['dispersions_norm'].values if n_top_genes is not None: dispersion_norm = dispersion_norm[~np.isnan(dispersion_norm)] - dispersion_norm[::-1].sort() # interestingly, np.argpartition is slightly slower + dispersion_norm[ + ::-1 + ].sort() # interestingly, np.argpartition is slightly slower if n_top_genes > adata.n_vars: logg.info('`n_top_genes` > `adata.n_var`, returning all genes.') n_top_genes = adata.n_vars disp_cut_off = dispersion_norm[n_top_genes - 1] gene_subset = np.nan_to_num(df['dispersions_norm'].values) >= disp_cut_off - logg.debug(f'the {n_top_genes} top genes correspond to a ' f'normalized dispersion cutoff of {disp_cut_off}') + logg.debug( + f'the {n_top_genes} top genes correspond to a ' + f'normalized dispersion cutoff of {disp_cut_off}' + ) else: dispersion_norm[np.isnan(dispersion_norm)] = 0 # similar to Seurat gene_subset = np.logical_and.reduce( @@ -385,7 +405,9 @@ def highly_variable_genes( This function replaces :func:`~scanpy.pp.filter_genes_dispersion`. """ - if n_top_genes is not None and not all(m is None for m in [min_disp, max_disp, min_mean, max_mean]): + if n_top_genes is not None and not all( + m is None for m in [min_disp, max_disp, min_mean, max_mean] + ): logg.info('If you pass `n_top_genes`, all cutoffs are ignored.') start = logg.info('extracting highly variable genes') @@ -471,8 +493,12 @@ def highly_variable_genes( highly_variable=np.nansum, ) ) - df.rename(columns=dict(highly_variable='highly_variable_nbatches'), inplace=True) - df['highly_variable_intersection'] = df['highly_variable_nbatches'] == len(batches) + df.rename( + columns=dict(highly_variable='highly_variable_nbatches'), inplace=True + ) + df['highly_variable_intersection'] = df['highly_variable_nbatches'] == len( + batches + ) if n_top_genes is not None: # sort genes by how often they selected as hvg within each batch and @@ -514,10 +540,16 @@ def highly_variable_genes( adata.var['highly_variable'] = df['highly_variable'].values adata.var['means'] = df['means'].values adata.var['dispersions'] = df['dispersions'].values - adata.var['dispersions_norm'] = df['dispersions_norm'].values.astype('float32', copy=False) + adata.var['dispersions_norm'] = df['dispersions_norm'].values.astype( + 'float32', copy=False + ) if batch_key is not None: - adata.var['highly_variable_nbatches'] = df['highly_variable_nbatches'].values - adata.var['highly_variable_intersection'] = df['highly_variable_intersection'].values + adata.var['highly_variable_nbatches'] = df[ + 'highly_variable_nbatches' + ].values + adata.var['highly_variable_intersection'] = df[ + 'highly_variable_intersection' + ].values if subset: adata._inplace_subset_var(df['highly_variable'].values) else: diff --git a/scanpy/preprocessing/_normalization.py b/scanpy/preprocessing/_normalization.py index 5bb4f7deb6..1931961693 100644 --- a/scanpy/preprocessing/_normalization.py +++ b/scanpy/preprocessing/_normalization.py @@ -133,7 +133,9 @@ def normalize_total( if layers == 'all': layers = adata.layers.keys() elif isinstance(layers, str): - raise ValueError(f"`layers` needs to be a list of strings or 'all', not {layers!r}") + raise ValueError( + f"`layers` needs to be a list of strings or 'all', not {layers!r}" + ) view_to_actual(adata) @@ -197,6 +199,8 @@ def normalize_total( time=start, ) if key_added is not None: - logg.debug(f'and added {key_added!r}, counts per cell before normalization (adata.obs)') + logg.debug( + f'and added {key_added!r}, counts per cell before normalization (adata.obs)' + ) return dat if not inplace else None diff --git a/scanpy/preprocessing/_pca.py b/scanpy/preprocessing/_pca.py index 12a03b60a9..fc4ec679cc 100644 --- a/scanpy/preprocessing/_pca.py +++ b/scanpy/preprocessing/_pca.py @@ -138,7 +138,9 @@ def pca( use_highly_variable = True if 'highly_variable' in adata.var.keys() else False if use_highly_variable: logg.info(' on highly variable genes') - adata_comp = adata[:, adata.var['highly_variable']] if use_highly_variable else adata + adata_comp = ( + adata[:, adata.var['highly_variable']] if use_highly_variable else adata + ) if n_comps is None: min_dim = min(adata_comp.n_vars, adata_comp.n_obs) @@ -180,7 +182,9 @@ def pca( "This may take a very large amount of memory." ) X = X.toarray() - pca_ = PCA(n_components=n_comps, svd_solver=svd_solver, random_state=random_state) + pca_ = PCA( + n_components=n_comps, svd_solver=svd_solver, random_state=random_state + ) X_pca = pca_.fit_transform(X) elif issparse(X) and zero_center: from sklearn.decomposition import PCA @@ -193,7 +197,9 @@ def pca( 'Use "arpack" (the default) or "lobpcg" instead.' ) - output = _pca_with_sparse(X, n_comps, solver=svd_solver, random_state=random_state) + output = _pca_with_sparse( + X, n_comps, solver=svd_solver, random_state=random_state + ) # this is just a wrapper for the results X_pca = output['X_pca'] pca_ = PCA(n_components=n_comps, svd_solver=svd_solver) @@ -209,7 +215,9 @@ def pca( ' the first component, e.g., might be heavily influenced by different means\n' ' the following components often resemble the exact PCA very closely' ) - pca_ = TruncatedSVD(n_components=n_comps, random_state=random_state, algorithm=svd_solver) + pca_ = TruncatedSVD( + n_components=n_comps, random_state=random_state, algorithm=svd_solver + ) X_pca = pca_.fit_transform(X) else: raise Exception("This shouldn't happen. Please open a bug report.") diff --git a/scanpy/preprocessing/_qc.py b/scanpy/preprocessing/_qc.py index f915fe738c..1f966f9d96 100644 --- a/scanpy/preprocessing/_qc.py +++ b/scanpy/preprocessing/_qc.py @@ -24,7 +24,8 @@ def _choose_mtx_rep(adata, use_raw=False, layer=None): is_layer = layer is not None if use_raw and is_layer: raise ValueError( - "Cannot use expression from both layer and raw. You provided:" f"'use_raw={use_raw}' and 'layer={layer}'" + "Cannot use expression from both layer and raw. You provided:" + f"'use_raw={use_raw}' and 'layer={layer}'" ) if is_layer: return adata.layers[layer] @@ -102,21 +103,33 @@ def describe_obs( else: obs_metrics[f"n_{var_type}_by_{expr_type}"] = np.count_nonzero(X, axis=1) if log1p: - obs_metrics[f"log1p_n_{var_type}_by_{expr_type}"] = np.log1p(obs_metrics[f"n_{var_type}_by_{expr_type}"]) + obs_metrics[f"log1p_n_{var_type}_by_{expr_type}"] = np.log1p( + obs_metrics[f"n_{var_type}_by_{expr_type}"] + ) obs_metrics[f"total_{expr_type}"] = X.sum(axis=1) if log1p: - obs_metrics[f"log1p_total_{expr_type}"] = np.log1p(obs_metrics[f"total_{expr_type}"]) + obs_metrics[f"log1p_total_{expr_type}"] = np.log1p( + obs_metrics[f"total_{expr_type}"] + ) if percent_top: percent_top = sorted(percent_top) proportions = top_segment_proportions(X, percent_top) for i, n in enumerate(percent_top): - obs_metrics[f"pct_{expr_type}_in_top_{n}_{var_type}"] = proportions[:, i] * 100 + obs_metrics[f"pct_{expr_type}_in_top_{n}_{var_type}"] = ( + proportions[:, i] * 100 + ) for qc_var in qc_vars: - obs_metrics[f"total_{expr_type}_{qc_var}"] = X[:, adata.var[qc_var].values].sum(axis=1) + obs_metrics[f"total_{expr_type}_{qc_var}"] = X[:, adata.var[qc_var].values].sum( + axis=1 + ) if log1p: - obs_metrics[f"log1p_total_{expr_type}_{qc_var}"] = np.log1p(obs_metrics[f"total_{expr_type}_{qc_var}"]) + obs_metrics[f"log1p_total_{expr_type}_{qc_var}"] = np.log1p( + obs_metrics[f"total_{expr_type}_{qc_var}"] + ) obs_metrics[f"pct_{expr_type}_{qc_var}"] = ( - obs_metrics[f"total_{expr_type}_{qc_var}"] / obs_metrics[f"total_{expr_type}"] * 100 + obs_metrics[f"total_{expr_type}_{qc_var}"] + / obs_metrics[f"total_{expr_type}"] + * 100 ) if inplace: adata.obs[obs_metrics.columns] = obs_metrics @@ -180,11 +193,17 @@ def describe_var( var_metrics["n_cells_by_{expr_type}"] = np.count_nonzero(X, axis=0) var_metrics["mean_{expr_type}"] = X.mean(axis=0) if log1p: - var_metrics["log1p_mean_{expr_type}"] = np.log1p(var_metrics["mean_{expr_type}"]) - var_metrics["pct_dropout_by_{expr_type}"] = (1 - var_metrics["n_cells_by_{expr_type}"] / X.shape[0]) * 100 + var_metrics["log1p_mean_{expr_type}"] = np.log1p( + var_metrics["mean_{expr_type}"] + ) + var_metrics["pct_dropout_by_{expr_type}"] = ( + 1 - var_metrics["n_cells_by_{expr_type}"] / X.shape[0] + ) * 100 var_metrics["total_{expr_type}"] = np.ravel(X.sum(axis=0)) if log1p: - var_metrics["log1p_total_{expr_type}"] = np.log1p(var_metrics["total_{expr_type}"]) + var_metrics["log1p_total_{expr_type}"] = np.log1p( + var_metrics["total_{expr_type}"] + ) # Relabel new_colnames = [] for col in var_metrics.columns: @@ -358,7 +377,9 @@ def top_proportions_sparse_csr(data, indptr, n): return values -def top_segment_proportions(mtx: Union[np.array, spmatrix], ns: Collection[int]) -> np.ndarray: +def top_segment_proportions( + mtx: Union[np.array, spmatrix], ns: Collection[int] +) -> np.ndarray: """ Calculates total percentage of counts in top ns genes. @@ -382,11 +403,15 @@ def top_segment_proportions(mtx: Union[np.array, spmatrix], ns: Collection[int]) return top_segment_proportions_dense(mtx, ns) -def top_segment_proportions_dense(mtx: Union[np.array, spmatrix], ns: Collection[int]) -> np.ndarray: +def top_segment_proportions_dense( + mtx: Union[np.array, spmatrix], ns: Collection[int] +) -> np.ndarray: # Currently ns is considered to be 1 indexed ns = np.sort(ns) sums = mtx.sum(axis=1) - partitioned = np.apply_along_axis(np.partition, 1, mtx, mtx.shape[1] - ns)[:, ::-1][:, : ns[-1]] + partitioned = np.apply_along_axis(np.partition, 1, mtx, mtx.shape[1] - ns)[:, ::-1][ + :, : ns[-1] + ] values = np.zeros((mtx.shape[0], len(ns))) acc = np.zeros(mtx.shape[0]) prev = 0 diff --git a/scanpy/preprocessing/_recipes.py b/scanpy/preprocessing/_recipes.py index 8d6f43b364..d211bcc20a 100644 --- a/scanpy/preprocessing/_recipes.py +++ b/scanpy/preprocessing/_recipes.py @@ -47,7 +47,9 @@ def recipe_weinreb17( adata = adata.copy() if log: pp.log1p(adata) - adata.X = normalize_per_cell_weinreb16_deprecated(adata.X, max_fraction=0.05, mult_with_mean=True) + adata.X = normalize_per_cell_weinreb16_deprecated( + adata.X, max_fraction=0.05, mult_with_mean=True + ) gene_subset = filter_genes_cv_deprecated(adata.X, mean_threshold, cv_threshold) adata._inplace_subset_var(gene_subset) # this modifies the object itself X_pca = pp.pca( @@ -61,7 +63,9 @@ def recipe_weinreb17( return adata if copy else None -def recipe_seurat(adata: AnnData, log: bool = True, plot: bool = False, copy: bool = False) -> Optional[AnnData]: +def recipe_seurat( + adata: AnnData, log: bool = True, plot: bool = False, copy: bool = False +) -> Optional[AnnData]: """\ Normalization and filtering as of Seurat [Satija15]_. @@ -75,7 +79,9 @@ def recipe_seurat(adata: AnnData, log: bool = True, plot: bool = False, copy: bo pp.filter_cells(adata, min_genes=200) pp.filter_genes(adata, min_cells=3) normalize_total(adata, target_sum=1e4) - filter_result = filter_genes_dispersion(adata.X, min_mean=0.0125, max_mean=3, min_disp=0.5, log=not log) + filter_result = filter_genes_dispersion( + adata.X, min_mean=0.0125, max_mean=3, min_disp=0.5, log=not log + ) if plot: from ..plotting import ( _preprocessing as ppp, @@ -146,7 +152,9 @@ def recipe_zheng17( pp.filter_genes(adata, min_counts=1) # normalize with total UMI count per cell normalize_total(adata, key_added='n_counts_all') - filter_result = filter_genes_dispersion(adata.X, flavor='cell_ranger', n_top_genes=n_top_genes, log=False) + filter_result = filter_genes_dispersion( + adata.X, flavor='cell_ranger', n_top_genes=n_top_genes, log=False + ) if plot: # should not import at the top of the file from ..plotting import _preprocessing as ppp diff --git a/scanpy/preprocessing/_simple.py b/scanpy/preprocessing/_simple.py index 70406533ec..cae9e7c581 100644 --- a/scanpy/preprocessing/_simple.py +++ b/scanpy/preprocessing/_simple.py @@ -119,7 +119,9 @@ def filter_cells( """ if copy: logg.warning('`copy` is deprecated, use `inplace` instead.') - n_given_options = sum(option is not None for option in [min_genes, min_counts, max_genes, max_counts]) + n_given_options = sum( + option is not None for option in [min_genes, min_counts, max_genes, max_counts] + ) if n_given_options != 1: raise ValueError( 'Only provide one of the optional parameters `min_counts`, ' @@ -141,7 +143,9 @@ def filter_cells( X = data # proceed with processing the data matrix min_number = min_counts if min_genes is None else min_genes max_number = max_counts if max_genes is None else max_genes - number_per_cell = np.sum(X if min_genes is None and max_genes is None else X > 0, axis=1) + number_per_cell = np.sum( + X if min_genes is None and max_genes is None else X > 0, axis=1 + ) if issparse(X): number_per_cell = number_per_cell.A1 if min_number is not None: @@ -154,10 +158,18 @@ def filter_cells( msg = f'filtered out {s} cells that have ' if min_genes is not None or min_counts is not None: msg += 'less than ' - msg += f'{min_genes} genes expressed' if min_counts is None else f'{min_counts} counts' + msg += ( + f'{min_genes} genes expressed' + if min_counts is None + else f'{min_counts} counts' + ) if max_genes is not None or max_counts is not None: msg += 'more than ' - msg += f'{max_genes} genes expressed' if max_counts is None else f'{max_counts} counts' + msg += ( + f'{max_genes} genes expressed' + if max_counts is None + else f'{max_counts} counts' + ) logg.info(msg) return cell_subset, number_per_cell @@ -211,7 +223,9 @@ def filter_genes( """ if copy: logg.warning('`copy` is deprecated, use `inplace` instead.') - n_given_options = sum(option is not None for option in [min_cells, min_counts, max_cells, max_counts]) + n_given_options = sum( + option is not None for option in [min_cells, min_counts, max_cells, max_counts] + ) if n_given_options != 1: raise ValueError( 'Only provide one of the optional parameters `min_counts`, ' @@ -241,7 +255,9 @@ def filter_genes( X = data # proceed with processing the data matrix min_number = min_counts if min_cells is None else min_cells max_number = max_counts if max_cells is None else max_cells - number_per_gene = np.sum(X if min_cells is None and max_cells is None else X > 0, axis=0) + number_per_gene = np.sum( + X if min_cells is None and max_cells is None else X > 0, axis=0 + ) if issparse(X): number_per_gene = number_per_gene.A1 if min_number is not None: @@ -254,10 +270,14 @@ def filter_genes( msg = f'filtered out {s} genes that are detected ' if min_cells is not None or min_counts is not None: msg += 'in less than ' - msg += f'{min_cells} cells' if min_counts is None else f'{min_counts} counts' + msg += ( + f'{min_cells} cells' if min_counts is None else f'{min_counts} counts' + ) if max_cells is not None or max_counts is not None: msg += 'in more than ' - msg += f'{max_cells} cells' if max_counts is None else f'{max_counts} counts' + msg += ( + f'{max_cells} cells' if max_counts is None else f'{max_counts} counts' + ) logg.info(msg) return gene_subset, number_per_gene @@ -303,13 +323,17 @@ def log1p( ------- Returns or updates `data`, depending on `copy`. """ - _check_array_function_arguments(chunked=chunked, chunk_size=chunk_size, layer=layer, obsm=obsm) + _check_array_function_arguments( + chunked=chunked, chunk_size=chunk_size, layer=layer, obsm=obsm + ) return log1p_array(X, copy=copy, base=base) @log1p.register(spmatrix) def log1p_sparse(X, *, base: Optional[Number] = None, copy: bool = False): - X = check_array(X, accept_sparse=("csr", "csc"), dtype=(np.float64, np.float32), copy=copy) + X = check_array( + X, accept_sparse=("csr", "csc"), dtype=(np.float64, np.float32), copy=copy + ) X.data = log1p(X.data, copy=False, base=base) return X @@ -323,7 +347,9 @@ def log1p_array(X, *, base: Optional[Number] = None, copy: bool = False): X = X.astype(np.floating) else: X = X.copy() - elif not (np.issubdtype(X.dtype, np.floating) or np.issubdtype(X.dtype, np.complex)): + elif not ( + np.issubdtype(X.dtype, np.floating) or np.issubdtype(X.dtype, np.complex) + ): X = X.astype(np.floating) np.log1p(X, out=X) if base is not None: @@ -350,7 +376,9 @@ def log1p_anndata( if chunked: if (layer is not None) or (obsm is not None): - raise NotImplementedError("Currently cannot perform chunked operations on arrays not stored in X.") + raise NotImplementedError( + "Currently cannot perform chunked operations on arrays not stored in X." + ) for chunk, start, end in adata.chunked_X(chunk_size): adata.X[start:end] = log1p(chunk, base=base, copy=False) else: @@ -492,7 +520,9 @@ def normalize_per_cell( start = logg.info('normalizing by total count per cell') adata = data.copy() if copy else data if counts_per_cell is None: - cell_subset, counts_per_cell = materialize_as_ndarray(filter_cells(adata.X, min_counts=min_counts)) + cell_subset, counts_per_cell = materialize_as_ndarray( + filter_cells(adata.X, min_counts=min_counts) + ) adata.obs[key_n_counts] = counts_per_cell adata._inplace_subset_obs(cell_subset) counts_per_cell = counts_per_cell[cell_subset] @@ -673,7 +703,9 @@ def _regress_out_chunk(data): else: regres = regressors try: - result = sm.GLM(data_chunk[:, col_index], regres, family=sm.families.Gaussian()).fit() + result = sm.GLM( + data_chunk[:, col_index], regres, family=sm.families.Gaussian() + ).fit() new_column = result.resid_response except PerfectSeparationError: # this emulates R's behavior logg.warning('Encountered PerfectSeparationError, setting to 0 as in R.') @@ -725,7 +757,9 @@ def scale( annotated with `'mean'` and `'std'` in `adata.var`. """ _check_array_function_arguments(layer=layer, obsm=obsm) - return scale_array(data, zero_center=zero_center, max_value=max_value, copy=copy) # noqa: F821 + return scale_array( + data, zero_center=zero_center, max_value=max_value, copy=copy # noqa: F821 + ) @scale.register(np.ndarray) @@ -745,7 +779,10 @@ def scale_array( ) if np.issubdtype(X.dtype, np.integer): - logg.info('... as scaling leads to float results, integer ' 'input is cast to float, returning copy.') + logg.info( + '... as scaling leads to float results, integer ' + 'input is cast to float, returning copy.' + ) X = X.astype(float) mean, var = _get_mean_var(X) @@ -782,7 +819,10 @@ def scale_sparse( ): # need to add the following here to make inplace logic work if zero_center: - logg.info("... as `zero_center=True`, sparse input is " "densified and may lead to large memory consumption") + logg.info( + "... as `zero_center=True`, sparse input is " + "densified and may lead to large memory consumption" + ) X = X.toarray() copy = False # Since the data has been copied return scale_array( @@ -916,7 +956,9 @@ def downsample_counts( total_counts_call = total_counts is not None counts_per_cell_call = counts_per_cell is not None if total_counts_call is counts_per_cell_call: - raise ValueError("Must specify exactly one of `total_counts` or `counts_per_cell`.") + raise ValueError( + "Must specify exactly one of `total_counts` or `counts_per_cell`." + ) if copy: adata = adata.copy() if total_counts_call: diff --git a/scanpy/preprocessing/_utils.py b/scanpy/preprocessing/_utils.py index 303a4d58d2..45ec781661 100644 --- a/scanpy/preprocessing/_utils.py +++ b/scanpy/preprocessing/_utils.py @@ -35,7 +35,9 @@ def sparse_mean_variance_axis(mtx: sparse.spmatrix, axis: int): else: raise ValueError("This function only works on sparse csr and csc matrices") if axis == ax_minor: - return sparse_mean_var_major_axis(mtx.data, mtx.indices, mtx.indptr, *shape, np.float64) + return sparse_mean_var_major_axis( + mtx.data, mtx.indices, mtx.indptr, *shape, np.float64 + ) else: return sparse_mean_var_minor_axis(mtx.data, mtx.indices, *shape, np.float64) diff --git a/scanpy/queries/_queries.py b/scanpy/queries/_queries.py index c206a5262e..7dff67565f 100644 --- a/scanpy/queries/_queries.py +++ b/scanpy/queries/_queries.py @@ -60,9 +60,13 @@ def simple_query( try: from pybiomart import Server except ImportError: - raise ImportError("This method requires the `pybiomart` module to be installed.") + raise ImportError( + "This method requires the `pybiomart` module to be installed." + ) server = Server(host, use_cache=use_cache) - dataset = server.marts["ENSEMBL_MART_ENSEMBL"].datasets["{}_gene_ensembl".format(org)] + dataset = server.marts["ENSEMBL_MART_ENSEMBL"].datasets[ + "{}_gene_ensembl".format(org) + ] res = dataset.query(attributes=attrs, filters=filters, use_attr_names=True) return res @@ -260,13 +264,16 @@ def enrich( try: from gprofiler import GProfiler except ImportError: - raise ImportError("This method requires the `gprofiler-official` module to be installed.") + raise ImportError( + "This method requires the `gprofiler-official` module to be installed." + ) gprofiler = GProfiler(user_agent="scanpy", return_dataframe=True) gprofiler_kwargs = dict(gprofiler_kwargs) for k in ["organism"]: if gprofiler_kwargs.get(k) is not None: raise ValueError( - f"Argument `{k}` should be passed directly through `enrich`, " "not through `gprofiler_kwargs`" + f"Argument `{k}` should be passed directly through `enrich`, " + "not through `gprofiler_kwargs`" ) return gprofiler.profile(container, organism=org, **gprofiler_kwargs) diff --git a/scanpy/readwrite.py b/scanpy/readwrite.py index c31f8f042f..87d2055fc4 100644 --- a/scanpy/readwrite.py +++ b/scanpy/readwrite.py @@ -214,7 +214,8 @@ def _read_legacy_10x_h5(filename, *, genome=None, start=None): genome = children[0] elif genome not in children: raise ValueError( - f"Could not find genome '{genome}' in '{filename}'. " f'Available genomes are: {children}' + f"Could not find genome '{genome}' in '{filename}'. " + f'Available genomes are: {children}' ) dsets = {} for node in f.walk_nodes('/' + genome, 'Array'): @@ -372,19 +373,26 @@ def read_visium( for f in files.values(): if not f.exists(): if any(x in str(f) for x in ["hires_image", "lowres_image"]): - logg.warning(f"You seem to be missing an image file.\n" f"Could not find '{f}'.") + logg.warning( + f"You seem to be missing an image file.\n" + f"Could not find '{f}'." + ) else: raise OSError(f"Could not find '{f}'") adata.uns["spatial"][library_id]['images'] = dict() for res in ['hires', 'lowres']: try: - adata.uns["spatial"][library_id]['images'][res] = imread(str(files[f'{res}_image'])) + adata.uns["spatial"][library_id]['images'][res] = imread( + str(files[f'{res}_image']) + ) except Exception: raise OSError(f"Could not find '{res}_image'") # read json scalefactors - adata.uns["spatial"][library_id]['scalefactors'] = json.loads(files['scalefactors_json_file'].read_bytes()) + adata.uns["spatial"][library_id]['scalefactors'] = json.loads( + files['scalefactors_json_file'].read_bytes() + ) adata.uns["spatial"][library_id]["metadata"] = { k: (str(attrs[k], "utf-8") if isinstance(attrs[k], bytes) else attrs[k]) @@ -406,7 +414,9 @@ def read_visium( adata.obs = adata.obs.join(positions, how="left") - adata.obsm['spatial'] = adata.obs[['pxl_row_in_fullres', 'pxl_col_in_fullres']].to_numpy() + adata.obsm['spatial'] = adata.obs[ + ['pxl_row_in_fullres', 'pxl_col_in_fullres'] + ].to_numpy() adata.obs.drop( columns=['barcode', 'pxl_row_in_fullres', 'pxl_col_in_fullres'], inplace=True, @@ -416,7 +426,9 @@ def read_visium( if source_image_path is not None: # get an absolute path source_image_path = str(Path(source_image_path).resolve()) - adata.uns["spatial"][library_id]["metadata"]["source_image_path"] = str(source_image_path) + adata.uns["spatial"][library_id]["metadata"]["source_image_path"] = str( + source_image_path + ) return adata @@ -477,7 +489,9 @@ def read_10x_mtx( if genefile_exists or not gex_only: return adata else: - gex_rows = list(map(lambda x: x == 'Gene Expression', adata.var['feature_types'])) + gex_rows = list( + map(lambda x: x == 'Gene Expression', adata.var['feature_types']) + ) return adata[:, gex_rows].copy() @@ -546,7 +560,9 @@ def _read_v3_10x_mtx( else: raise ValueError("`var_names` needs to be 'gene_symbols' or 'gene_ids'") adata.var['feature_types'] = genes[2].values - adata.obs_names = pd.read_csv(path / f'{prefix}barcodes.tsv.gz', header=None)[0].values + adata.obs_names = pd.read_csv(path / f'{prefix}barcodes.tsv.gz', header=None)[ + 0 + ].values return adata @@ -596,7 +612,9 @@ def write( if ext == 'csv': adata.write_csvs(filename) else: - adata.write(filename, compression=compression, compression_opts=compression_opts) + adata.write( + filename, compression=compression, compression_opts=compression_opts + ) # ------------------------------------------------------------------------------- @@ -604,7 +622,9 @@ def write( # ------------------------------------------------------------------------------- -def read_params(filename: Union[Path, str], asheader: bool = False) -> Dict[str, Union[int, float, bool, str, None]]: +def read_params( + filename: Union[Path, str], asheader: bool = False +) -> Dict[str, Union[int, float, bool, str, None]]: """\ Read parameter dictionary from text file. @@ -679,7 +699,9 @@ def _read( **kwargs, ): if ext is not None and ext not in avail_exts: - raise ValueError('Please provide one of the available extensions.\n' f'{avail_exts}') + raise ValueError( + 'Please provide one of the available extensions.\n' f'{avail_exts}' + ) else: ext = is_valid_filename(filename, return_ext=True) is_present = _check_datafile_present_and_download(filename, backup_url=backup_url) @@ -693,7 +715,9 @@ def _read( logg.debug(f'reading sheet {sheet} from file {filename}') return read_hdf(filename, sheet) # read other file types - path_cache = settings.cachedir / _slugify(filename).replace('.' + ext, '.h5ad') # type: Path + path_cache = settings.cachedir / _slugify(filename).replace( + '.' + ext, '.h5ad' + ) # type: Path if path_cache.suffix in {'.gz', '.bz2'}: path_cache = path_cache.with_suffix('') if cache and path_cache.is_file(): @@ -732,7 +756,10 @@ def _read( else: raise ValueError(f'Unknown extension {ext}.') if cache: - logg.info(f'... writing an {settings.file_format_data} ' 'cache file to speedup reading next time') + logg.info( + f'... writing an {settings.file_format_data} ' + 'cache file to speedup reading next time' + ) if cache_compression is _empty: cache_compression = settings.cache_compression if not path_cache.parent.is_dir(): @@ -876,7 +903,9 @@ def get_used_files(): """Get files used by processes with name scanpy.""" import psutil - loop_over_scanpy_processes = (proc for proc in psutil.process_iter() if proc.name() == 'scanpy') + loop_over_scanpy_processes = ( + proc for proc in psutil.process_iter() if proc.name() == 'scanpy' + ) filenames = [] for proc in loop_over_scanpy_processes: try: @@ -938,7 +967,10 @@ def _check_datafile_present_and_download(path, backup_url=None): return True if backup_url is None: return False - logg.info(f'try downloading from url\n{backup_url}\n' '... this may take a while but only happens once') + logg.info( + f'try downloading from url\n{backup_url}\n' + '... this may take a while but only happens once' + ) if not path.parent.is_dir(): logg.info(f'creating directory {path.parent}/ for saving data') path.parent.mkdir(parents=True) @@ -953,7 +985,8 @@ def is_valid_filename(filename: Path, return_ext=False): if len(ext) > 2: logg.warning( - f'Your filename has more than two extensions: {ext}.\n' f'Only considering the two last: {ext[-2:]}.' + f'Your filename has more than two extensions: {ext}.\n' + f'Only considering the two last: {ext[-2:]}.' ) ext = ext[-2:] diff --git a/scanpy/tests/external/test_hashsolo.py b/scanpy/tests/external/test_hashsolo.py index 4d3a223f28..8ab8df0e61 100644 --- a/scanpy/tests/external/test_hashsolo.py +++ b/scanpy/tests/external/test_hashsolo.py @@ -23,7 +23,9 @@ def test_cell_demultiplexing(): sce.pp.hashsolo(test_data, test_data.obs.columns) doublets = ["Doublet"] * 10 - classes = list(np.repeat(np.arange(10), 98).reshape(98, 10, order="F").ravel().astype(str)) + classes = list( + np.repeat(np.arange(10), 98).reshape(98, 10, order="F").ravel().astype(str) + ) negatives = ["Negative"] * 10 classification = doublets + classes + negatives assert all(test_data.obs["Classification"].astype(str) == classification) diff --git a/scanpy/tests/external/test_scrublet.py b/scanpy/tests/external/test_scrublet.py index 30fc66a32a..b67b479979 100644 --- a/scanpy/tests/external/test_scrublet.py +++ b/scanpy/tests/external/test_scrublet.py @@ -16,8 +16,6 @@ def test_scrublet(): adata = sc.datasets.pbmc3k() sce.pp.scrublet(adata, use_approx_neighbors=False) - errors = [] - # replace assertions by conditions assert "predicted_doublet" in adata.obs.columns assert "doublet_score" in adata.obs.columns @@ -82,7 +80,7 @@ def test_scrublet_simulate_doublets(): sc.pp.normalize_total(adata_obs) logged = sc.pp.log1p(adata_obs, copy=True) - hvg = sc.pp.highly_variable_genes(logged) + _ = sc.pp.highly_variable_genes(logged) adata_obs = adata_obs[:, logged.var['highly_variable']] adata_sim = sce.pp.scrublet_simulate_doublets(adata_obs, layer='raw') diff --git a/scanpy/tests/external/test_wishbone.py b/scanpy/tests/external/test_wishbone.py index 9baca5f877..fc3cf71901 100644 --- a/scanpy/tests/external/test_wishbone.py +++ b/scanpy/tests/external/test_wishbone.py @@ -20,4 +20,6 @@ def test_run_wishbone(): components=[2, 3], num_waypoints=150, ) - assert all([k in adata.obs for k in ['trajectory_wishbone', 'branch_wishbone']]), "Run Wishbone Error!" + assert all( + [k in adata.obs for k in ['trajectory_wishbone', 'branch_wishbone']] + ), "Run Wishbone Error!" diff --git a/scanpy/tests/helpers.py b/scanpy/tests/helpers.py index dcb2f646d5..35253bad69 100644 --- a/scanpy/tests/helpers.py +++ b/scanpy/tests/helpers.py @@ -30,7 +30,9 @@ def check_rep_mutation(func, X, **kwargs): assert np.array_equal(asarray(adata_layer.X), asarray(adata_layer.obsm["obsm"])) assert np.array_equal(asarray(adata_obsm.X), asarray(adata_obsm.layers["layer"])) - assert np.array_equal(asarray(adata_X.layers["layer"]), asarray(adata_X.obsm["obsm"])) + assert np.array_equal( + asarray(adata_X.layers["layer"]), asarray(adata_X.obsm["obsm"]) + ) def check_rep_results(func, X, **kwargs): diff --git a/scanpy/tests/notebooks/test_paga_paul15_subsampled.py b/scanpy/tests/notebooks/test_paga_paul15_subsampled.py index cbf7c0d252..839d93f40a 100644 --- a/scanpy/tests/notebooks/test_paga_paul15_subsampled.py +++ b/scanpy/tests/notebooks/test_paga_paul15_subsampled.py @@ -115,7 +115,9 @@ def test_paga_paul15_subsampled(image_comparer, plt): adata.obs['distance'] = adata.obs['dpt_pseudotime'] - _, axs = plt.subplots(ncols=3, figsize=(6, 2.5), gridspec_kw={'wspace': 0.05, 'left': 0.12}) + _, axs = plt.subplots( + ncols=3, figsize=(6, 2.5), gridspec_kw={'wspace': 0.05, 'left': 0.12} + ) plt.subplots_adjust(left=0.05, right=0.98, top=0.82, bottom=0.2) for ipath, (descr, path) in enumerate(paths): _, data = sc.pl.paga_path( diff --git a/scanpy/tests/notebooks/test_pbmc3k.py b/scanpy/tests/notebooks/test_pbmc3k.py index b7198d972b..0146c4b4de 100644 --- a/scanpy/tests/notebooks/test_pbmc3k.py +++ b/scanpy/tests/notebooks/test_pbmc3k.py @@ -30,7 +30,9 @@ def test_pbmc3k(image_comparer): save_and_compare_images = image_comparer(ROOT, FIGS, tol=20) - adata = sc.read('./data/pbmc3k_raw.h5ad', backup_url='http://falexwolf.de/data/pbmc3k_raw.h5ad') + adata = sc.read( + './data/pbmc3k_raw.h5ad', backup_url='http://falexwolf.de/data/pbmc3k_raw.h5ad' + ) # Preprocessing @@ -43,7 +45,9 @@ def test_pbmc3k(image_comparer): mito_genes = [name for name in adata.var_names if name.startswith('MT-')] # for each cell compute fraction of counts in mito genes vs. all genes # the `.A1` is only necessary as X is sparse to transform to a dense array after summing - adata.obs['percent_mito'] = np.sum(adata[:, mito_genes].X, axis=1).A1 / np.sum(adata.X, axis=1).A1 + adata.obs['percent_mito'] = ( + np.sum(adata[:, mito_genes].X, axis=1).A1 / np.sum(adata.X, axis=1).A1 + ) # add the total counts per cell as observations-annotation to adata adata.obs['n_counts'] = adata.X.sum(axis=1).A1 @@ -140,5 +144,7 @@ def test_pbmc3k(image_comparer): # sc.pl.umap(adata, color='louvain', legend_loc='on data', title='', frameon=False, show=False) # save_and_compare_images('umap_3') - sc.pl.violin(adata, ['CST3', 'NKG7', 'PPBP'], groupby='louvain', rotation=90, show=False) + sc.pl.violin( + adata, ['CST3', 'NKG7', 'PPBP'], groupby='louvain', rotation=90, show=False + ) save_and_compare_images('violin_2') diff --git a/scanpy/tests/test_combat.py b/scanpy/tests/test_combat.py index 79be9a10ea..295667b909 100644 --- a/scanpy/tests/test_combat.py +++ b/scanpy/tests/test_combat.py @@ -37,7 +37,9 @@ def test_covariates(): adata.obs['cat2'] = np.random.binomial(2, 0.1, size=(adata.n_obs)) adata.obs['num1'] = np.random.normal(size=(adata.n_obs)) - X2 = sc.pp.combat(adata, key=key, covariates=['cat1', 'cat2', 'num1'], inplace=False) + X2 = sc.pp.combat( + adata, key=key, covariates=['cat1', 'cat2', 'num1'], inplace=False + ) sc.pp.combat(adata, key=key, covariates=['cat1', 'cat2', 'num1'], inplace=True) assert X1.shape == X2.shape diff --git a/scanpy/tests/test_datasets.py b/scanpy/tests/test_datasets.py index 30e3d497a8..50332de6f9 100644 --- a/scanpy/tests/test_datasets.py +++ b/scanpy/tests/test_datasets.py @@ -63,7 +63,10 @@ def test_ebi_expression_atlas(tmp_dataset_dir): def test_krumsiek11(tmp_dataset_dir): adata = sc.datasets.krumsiek11() assert adata.shape == (640, 11) - assert all(np.unique(adata.obs["cell_type"]) == np.array(["Ery", "Mk", "Mo", "Neu", "progenitor"])) + assert all( + np.unique(adata.obs["cell_type"]) + == np.array(["Ery", "Mk", "Mo", "Neu", "progenitor"]) + ) def test_blobs(): @@ -99,14 +102,18 @@ def test_visium_datasets(tmp_dataset_dir, tmpdir): # Test that downloading tissue image works mbrain = sc.datasets.visium_sge("V1_Adult_Mouse_Brain", include_hires_tiff=True) expected_image_path = sc.settings.datasetdir / "V1_Adult_Mouse_Brain" / "image.tif" - image_path = Path(mbrain.uns["spatial"]["V1_Adult_Mouse_Brain"]["metadata"]["source_image_path"]) + image_path = Path( + mbrain.uns["spatial"]["V1_Adult_Mouse_Brain"]["metadata"]["source_image_path"] + ) assert image_path == expected_image_path # Test that tissue image exists and is a valid image file assert image_path.exists() # Test that tissue image is a tif image file (using `file`) - process = subprocess.run(['file', '--mime-type', image_path], stdout=subprocess.PIPE) + process = subprocess.run( + ['file', '--mime-type', image_path], stdout=subprocess.PIPE + ) output = process.stdout.strip().decode() # make process output string assert output == str(image_path) + ': image/tiff' diff --git a/scanpy/tests/test_docs.py b/scanpy/tests/test_docs.py index be826214b6..0c82178be5 100644 --- a/scanpy/tests/test_docs.py +++ b/scanpy/tests/test_docs.py @@ -9,7 +9,9 @@ scanpy_functions = [ - c_or_f for c_or_f in descend_classes_and_funcs(scanpy, "scanpy") if isinstance(c_or_f, FunctionType) + c_or_f + for c_or_f in descend_classes_and_funcs(scanpy, "scanpy") + if isinstance(c_or_f, FunctionType) ] diff --git a/scanpy/tests/test_embedding_plots.py b/scanpy/tests/test_embedding_plots.py index c1515d2ba2..49746e45c3 100644 --- a/scanpy/tests/test_embedding_plots.py +++ b/scanpy/tests/test_embedding_plots.py @@ -29,7 +29,9 @@ def adata(): from sklearn.cluster import DBSCAN empty_pixel = np.array([1.0, 1.0, 1.0, 0]).reshape(1, 1, -1) - image = imread(Path(sc.__file__).parent.parent / "docs/_static/img/Scanpy_Logo_RGB.png") + image = imread( + Path(sc.__file__).parent.parent / "docs/_static/img/Scanpy_Logo_RGB.png" + ) x, y = np.where(np.logical_and.reduce(~np.equal(image, empty_pixel), axis=2)) # Just using to calculate the hex coords @@ -68,7 +70,9 @@ def adata(): adata.obs["label_missing"][::2] = np.nan adata.obs["1_missing"] = adata.obs_vector("1") - adata.obs.loc[adata.obsm["spatial"][:, 0] < adata.obsm["spatial"][:, 0].mean(), "1_missing"] = np.nan + adata.obs.loc[ + adata.obsm["spatial"][:, 0] < adata.obsm["spatial"][:, 0].mean(), "1_missing" + ] = np.nan return adata @@ -137,7 +141,9 @@ def test_missing_values_categorical( legend_loc, groupsfunc, ): - save_and_compare_images = image_comparer(MISSING_VALUES_ROOT, MISSING_VALUES_FIGS, tol=15) + save_and_compare_images = image_comparer( + MISSING_VALUES_ROOT, MISSING_VALUES_FIGS, tol=15 + ) base_name = fixture_request.node.name # Passing through a dict so it's easier to use default values @@ -153,8 +159,12 @@ def test_missing_values_categorical( save_and_compare_images(base_name) -def test_missing_values_continuous(fixture_request, image_comparer, adata, plotfunc, na_color, legend_loc, vbounds): - save_and_compare_images = image_comparer(MISSING_VALUES_ROOT, MISSING_VALUES_FIGS, tol=15) +def test_missing_values_continuous( + fixture_request, image_comparer, adata, plotfunc, na_color, legend_loc, vbounds +): + save_and_compare_images = image_comparer( + MISSING_VALUES_ROOT, MISSING_VALUES_FIGS, tol=15 + ) base_name = fixture_request.node.name # Passing through a dict so it's easier to use default values @@ -238,9 +248,13 @@ def test_spatial_general(image_comparer): # general coordinates save_and_compare_images = image_comparer(ROOT, FIGS, tol=15) adata = sc.read_visium(HERE / '_data' / 'visium_data' / '1.0.0') adata.obs = adata.obs.astype({'array_row': 'str'}) - spatial_metadata = adata.uns.pop("spatial") # spatial data don't have imgs, so remove entry from uns + spatial_metadata = adata.uns.pop( + "spatial" + ) # spatial data don't have imgs, so remove entry from uns # Required argument for now - spot_size = list(spatial_metadata.values())[0]["scalefactors"]["spot_diameter_fullres"] + spot_size = list(spatial_metadata.values())[0]["scalefactors"][ + "spot_diameter_fullres" + ] sc.pl.spatial(adata, show=False, spot_size=spot_size) save_and_compare_images('master_spatial_general_nocol') @@ -313,8 +327,12 @@ def equivalent_spatial_plotters_no_img(equivalent_spatial_plotters): pytest.param({"bw": True}, id="bw"), # Shape of the image for particular fixture, should not be hardcoded like this pytest.param({"img": np.ones((774, 1755, 4)), "scale_factor": 1.0}, id="img"), - pytest.param({"na_color": (0, 0, 0, 0), "color": "1_missing"}, id="na_color.transparent"), - pytest.param({"na_color": "lightgray", "color": "1_missing"}, id="na_color.lightgray"), + pytest.param( + {"na_color": (0, 0, 0, 0), "color": "1_missing"}, id="na_color.transparent" + ), + pytest.param( + {"na_color": "lightgray", "color": "1_missing"}, id="na_color.lightgray" + ), ] ) def spatial_kwargs(request): @@ -341,7 +359,9 @@ def test_manual_equivalency(equivalent_spatial_plotters, tmpdir, spatial_kwargs) check_images(orig_pth, removed_pth, tol=1) -def test_manual_equivalency_no_img(equivalent_spatial_plotters_no_img, tmpdir, spatial_kwargs): +def test_manual_equivalency_no_img( + equivalent_spatial_plotters_no_img, tmpdir, spatial_kwargs +): if "bw" in spatial_kwargs: # Has no meaning when there is no image pytest.skip() @@ -366,7 +386,9 @@ def test_white_background_vs_no_img(adata, tmpdir, spatial_kwargs): # These arguments don't make sense for this check pytest.skip() - white_background = np.ones_like(adata.uns["spatial"]["scanpy_img"]["images"]["hires"]) + white_background = np.ones_like( + adata.uns["spatial"]["scanpy_img"]["images"]["hires"] + ) TESTDIR = Path(tmpdir) white_pth = TESTDIR / "white_background.png" noimg_pth = TESTDIR / "no_img.png" @@ -390,7 +412,9 @@ def test_spatial_na_color(adata, tmpdir): """ Check that na_color defaults to transparent when an image is present, light gray when not. """ - white_background = np.ones_like(adata.uns["spatial"]["scanpy_img"]["images"]["hires"]) + white_background = np.ones_like( + adata.uns["spatial"]["scanpy_img"]["images"]["hires"] + ) TESTDIR = Path(tmpdir) lightgray_pth = TESTDIR / "lightgray.png" transparent_pth = TESTDIR / "transparent.png" diff --git a/scanpy/tests/test_filter_rank_genes_groups.py b/scanpy/tests/test_filter_rank_genes_groups.py index 9989a81989..91aff2d27c 100644 --- a/scanpy/tests/test_filter_rank_genes_groups.py +++ b/scanpy/tests/test_filter_rank_genes_groups.py @@ -47,7 +47,9 @@ def test_filter_rank_genes_groups(): 'max_out_group_fraction': 0.5, } - rank_genes_groups(adata, 'bulk_labels', reference='Dendritic', method='wilcoxon', n_genes=5) + rank_genes_groups( + adata, 'bulk_labels', reference='Dendritic', method='wilcoxon', n_genes=5 + ) filter_rank_genes_groups(**args) assert np.array_equal( diff --git a/scanpy/tests/test_get.py b/scanpy/tests/test_get.py index 1050e4ad55..7177fee921 100644 --- a/scanpy/tests/test_get.py +++ b/scanpy/tests/test_get.py @@ -39,8 +39,12 @@ def adata(): """ return AnnData( X=np.ones((2, 2)), - obs=pd.DataFrame({"obs1": [0, 1], "obs2": ["a", "b"]}, index=["cell1", "cell2"]), - var=pd.DataFrame({"gene_symbols": ["genesymbol1", "genesymbol2"]}, index=["gene1", "gene2"]), + obs=pd.DataFrame( + {"obs1": [0, 1], "obs2": ["a", "b"]}, index=["cell1", "cell2"] + ), + var=pd.DataFrame( + {"gene_symbols": ["genesymbol1", "genesymbol2"]}, index=["gene1", "gene2"] + ), layers={"double": sparse.csr_matrix(np.ones((2, 2)), dtype=int) * 2}, dtype=int, ) @@ -65,7 +69,9 @@ def test_obs_df(adata): dtype='float64', ) pd.testing.assert_frame_equal( - sc.get.obs_df(adata, keys=["gene2", "obs1"], obsm_keys=[("eye", 0), ("sparse", 1)]), + sc.get.obs_df( + adata, keys=["gene2", "obs1"], obsm_keys=[("eye", 0), ("sparse", 1)] + ), pd.DataFrame( {"gene2": [1, 1], "obs1": [0, 1], "eye-0": [1, 0], "sparse-1": [0.0, 1.0]}, index=adata.obs_names, @@ -355,7 +361,9 @@ def test_repeated_cols(dim, transform, func): adata = transform( sc.AnnData( np.ones((5, 10)), - obs=pd.DataFrame(np.ones((5, 2)), columns=["a_column_name", "a_column_name"]), + obs=pd.DataFrame( + np.ones((5, 2)), columns=["a_column_name", "a_column_name"] + ), var=pd.DataFrame(index=[f"gene-{i}" for i in range(10)]), ) ) @@ -372,7 +380,9 @@ def test_repeated_index_vals(dim, transform, func): adata = transform( sc.AnnData( np.ones((5, 10)), - var=pd.DataFrame(index=["repeated_id"] * 2 + [f"gene-{i}" for i in range(8)]), + var=pd.DataFrame( + index=["repeated_id"] * 2 + [f"gene-{i}" for i in range(8)] + ), ) ) diff --git a/scanpy/tests/test_highly_variable_genes.py b/scanpy/tests/test_highly_variable_genes.py index 587d895781..8b3e4f52c2 100644 --- a/scanpy/tests/test_highly_variable_genes.py +++ b/scanpy/tests/test_highly_variable_genes.py @@ -63,9 +63,13 @@ def test_higly_variable_genes_compare_to_seurat(): sc.pp.normalize_per_cell(pbmc, counts_per_cell_after=1e4) sc.pp.log1p(pbmc) - sc.pp.highly_variable_genes(pbmc, flavor='seurat', min_mean=0.0125, max_mean=3, min_disp=0.5, inplace=True) + sc.pp.highly_variable_genes( + pbmc, flavor='seurat', min_mean=0.0125, max_mean=3, min_disp=0.5, inplace=True + ) - np.testing.assert_array_equal(seurat_hvg_info['highly_variable'], pbmc.var['highly_variable']) + np.testing.assert_array_equal( + seurat_hvg_info['highly_variable'], pbmc.var['highly_variable'] + ) # (still) Not equal to tolerance rtol=2e-05, atol=2e-05 # np.testing.assert_allclose(4, 3.9999, rtol=2e-05, atol=2e-05) @@ -90,7 +94,9 @@ def test_higly_variable_genes_compare_to_seurat(): def test_higly_variable_genes_compare_to_seurat_v3(): - seurat_hvg_info = pd.read_csv(FILE_V3, sep=' ', dtype={"variances_norm": np.float64}) + seurat_hvg_info = pd.read_csv( + FILE_V3, sep=' ', dtype={"variances_norm": np.float64} + ) pbmc = sc.datasets.pbmc3k() pbmc.var_names_make_unique() @@ -101,7 +107,9 @@ def test_higly_variable_genes_compare_to_seurat_v3(): sc.pp.highly_variable_genes(pbmc, n_top_genes=1000, flavor='seurat_v3') sc.pp.highly_variable_genes(pbmc_dense, n_top_genes=1000, flavor='seurat_v3') - np.testing.assert_array_equal(seurat_hvg_info['highly_variable'], pbmc.var['highly_variable']) + np.testing.assert_array_equal( + seurat_hvg_info['highly_variable'], pbmc.var['highly_variable'] + ) np.testing.assert_allclose( seurat_hvg_info['variances'], pbmc.var['variances'], @@ -124,7 +132,9 @@ def test_higly_variable_genes_compare_to_seurat_v3(): batch = np.zeros((len(pbmc)), dtype=int) batch[1500:] = 1 pbmc.obs["batch"] = batch - df = sc.pp.highly_variable_genes(pbmc, n_top_genes=4000, flavor='seurat_v3', batch_key="batch", inplace=False) + df = sc.pp.highly_variable_genes( + pbmc, n_top_genes=4000, flavor='seurat_v3', batch_key="batch", inplace=False + ) df.sort_values( ["highly_variable_nbatches", "highly_variable_rank"], ascending=[False, True], @@ -132,7 +142,9 @@ def test_higly_variable_genes_compare_to_seurat_v3(): inplace=True, ) df = df.iloc[:4000] - seurat_hvg_info_batch = pd.read_csv(FILE_V3_BATCH, sep=' ', dtype={"variances_norm": np.float64}) + seurat_hvg_info_batch = pd.read_csv( + FILE_V3_BATCH, sep=' ', dtype={"variances_norm": np.float64} + ) # ranks might be slightly different due to many genes having same normalized var seu = pd.Index(seurat_hvg_info_batch['x'].values) @@ -164,7 +176,9 @@ def test_filter_genes_dispersion_compare_to_seurat(): min_disp=0.5, ) - np.testing.assert_array_equal(seurat_hvg_info['highly_variable'], pbmc.var['highly_variable']) + np.testing.assert_array_equal( + seurat_hvg_info['highly_variable'], pbmc.var['highly_variable'] + ) # (still) Not equal to tolerance rtol=2e-05, atol=2e-05: # np.testing.assert_allclose(4, 3.9999, rtol=2e-05, atol=2e-05) @@ -205,8 +219,12 @@ def test_highly_variable_genes_batches(): sc.pp.filter_genes(adata_1, min_cells=1) sc.pp.filter_genes(adata_2, min_cells=1) - hvg1 = sc.pp.highly_variable_genes(adata_1, flavor='cell_ranger', n_top_genes=200, inplace=False) - hvg2 = sc.pp.highly_variable_genes(adata_2, flavor='cell_ranger', n_top_genes=200, inplace=False) + hvg1 = sc.pp.highly_variable_genes( + adata_1, flavor='cell_ranger', n_top_genes=200, inplace=False + ) + hvg2 = sc.pp.highly_variable_genes( + adata_2, flavor='cell_ranger', n_top_genes=200, inplace=False + ) assert np.isclose( adata.var['dispersions_norm'][100], @@ -216,7 +234,9 @@ def test_highly_variable_genes_batches(): adata.var['dispersions_norm'][101], 0.5 * hvg1['dispersions_norm'][1] + 0.5 * hvg2['dispersions_norm'][101], ) - assert np.isclose(adata.var['dispersions_norm'][0], 0.5 * hvg2['dispersions_norm'][0]) + assert np.isclose( + adata.var['dispersions_norm'][0], 0.5 * hvg2['dispersions_norm'][0] + ) colnames = [ 'means', diff --git a/scanpy/tests/test_ingest.py b/scanpy/tests/test_ingest.py index bf3ad73605..a7ba765f98 100644 --- a/scanpy/tests/test_ingest.py +++ b/scanpy/tests/test_ingest.py @@ -137,7 +137,9 @@ def test_ingest_map_embedding_umap(): adata_ref = sc.AnnData(X) adata_new = sc.AnnData(T) - sc.pp.neighbors(adata_ref, method='umap', use_rep='X', n_neighbors=4, random_state=0) + sc.pp.neighbors( + adata_ref, method='umap', use_rep='X', n_neighbors=4, random_state=0 + ) sc.tl.umap(adata_ref, random_state=0) ing = sc.tl.Ingest(adata_ref) diff --git a/scanpy/tests/test_neighbors.py b/scanpy/tests/test_neighbors.py index 468b02fdd4..72df4fbf1a 100644 --- a/scanpy/tests/test_neighbors.py +++ b/scanpy/tests/test_neighbors.py @@ -143,9 +143,13 @@ def test_gauss_connectivities_euclidean(neigh): def test_metrics_argument(): no_knn_euclidean = get_neighbors() - no_knn_euclidean.compute_neighbors(method="gauss", knn=False, n_neighbors=n_neighbors, metric="euclidean") + no_knn_euclidean.compute_neighbors( + method="gauss", knn=False, n_neighbors=n_neighbors, metric="euclidean" + ) no_knn_manhattan = get_neighbors() - no_knn_manhattan.compute_neighbors(method="gauss", knn=False, n_neighbors=n_neighbors, metric="manhattan") + no_knn_manhattan.compute_neighbors( + method="gauss", knn=False, n_neighbors=n_neighbors, metric="manhattan" + ) assert not np.allclose(no_knn_euclidean.distances, no_knn_manhattan.distances) diff --git a/scanpy/tests/test_neighbors_key_added.py b/scanpy/tests/test_neighbors_key_added.py index db786e170c..6793a40d15 100644 --- a/scanpy/tests/test_neighbors_key_added.py +++ b/scanpy/tests/test_neighbors_key_added.py @@ -19,8 +19,12 @@ def test_neighbors_key_added(adata): dists_key = adata.uns[key]['distances_key'] assert adata.uns['neighbors']['params'] == adata.uns[key]['params'] - assert np.allclose(adata.obsp['connectivities'].toarray(), adata.obsp[conns_key].toarray()) - assert np.allclose(adata.obsp['distances'].toarray(), adata.obsp[dists_key].toarray()) + assert np.allclose( + adata.obsp['connectivities'].toarray(), adata.obsp[conns_key].toarray() + ) + assert np.allclose( + adata.obsp['distances'].toarray(), adata.obsp[dists_key].toarray() + ) # test functions with neighbors_key and obsp diff --git a/scanpy/tests/test_pca.py b/scanpy/tests/test_pca.py index d645abbc03..2dc827c2f6 100644 --- a/scanpy/tests/test_pca.py +++ b/scanpy/tests/test_pca.py @@ -1,11 +1,8 @@ import pytest import numpy as np from anndata import AnnData -from scipy.sparse import csr_matrix -from scipy import sparse import scanpy as sc -from scanpy.tests.fixtures import array_type, float_dtype from anndata.tests.helpers import assert_equal A_list = [ @@ -94,7 +91,9 @@ def test_pca_sparse(pbmc3k_normalized): explicit = sc.pp.pca(pbmc_dense, dtype=np.float64, copy=True) assert np.allclose(implicit.uns["pca"]["variance"], explicit.uns["pca"]["variance"]) - assert np.allclose(implicit.uns["pca"]["variance_ratio"], explicit.uns["pca"]["variance_ratio"]) + assert np.allclose( + implicit.uns["pca"]["variance_ratio"], explicit.uns["pca"]["variance_ratio"] + ) assert np.allclose(implicit.obsm['X_pca'], explicit.obsm['X_pca']) assert np.allclose(implicit.varm['PCs'], explicit.varm['PCs']) @@ -125,9 +124,13 @@ def test_pca_chunked(pbmc3k_normalized): default = sc.pp.pca(pbmc3k_normalized, copy=True) # Taking absolute value since sometimes dimensions are flipped - np.testing.assert_allclose(np.abs(chunked.obsm["X_pca"]), np.abs(default.obsm["X_pca"])) + np.testing.assert_allclose( + np.abs(chunked.obsm["X_pca"]), np.abs(default.obsm["X_pca"]) + ) np.testing.assert_allclose(np.abs(chunked.varm["PCs"]), np.abs(default.varm["PCs"])) - np.testing.assert_allclose(np.abs(chunked.uns["pca"]["variance"]), np.abs(default.uns["pca"]["variance"])) + np.testing.assert_allclose( + np.abs(chunked.uns["pca"]["variance"]), np.abs(default.uns["pca"]["variance"]) + ) np.testing.assert_allclose( np.abs(chunked.uns["pca"]["variance_ratio"]), np.abs(default.uns["pca"]["variance_ratio"]), diff --git a/scanpy/tests/test_plotting.py b/scanpy/tests/test_plotting.py index 265c7b8220..8eee2c576e 100644 --- a/scanpy/tests/test_plotting.py +++ b/scanpy/tests/test_plotting.py @@ -134,10 +134,14 @@ def test_heatmap(image_comparer): var=pd.DataFrame({"genes": 'g1 g2 g3'.split()}).set_index('genes'), ) a.obs['foo'] = a.obs['foo'].astype('category') - sc.pl.heatmap(a, var_names=a.var_names, groupby='foo', swap_axes=True, figsize=(4, 4)) + sc.pl.heatmap( + a, var_names=a.var_names, groupby='foo', swap_axes=True, figsize=(4, 4) + ) save_and_compare_images('master_heatmap_small_swap_alignment') - sc.pl.heatmap(a, var_names=a.var_names, groupby='foo', swap_axes=False, figsize=(4, 4)) + sc.pl.heatmap( + a, var_names=a.var_names, groupby='foo', swap_axes=False, figsize=(4, 4) + ) save_and_compare_images('master_heatmap_small_alignment') @@ -161,7 +165,9 @@ def test_clustermap(image_comparer, obs_keys, name): [ ( "dotplot", - partial(sc.pl.dotplot, groupby='cell_type', title='dotplot', dendrogram=True), + partial( + sc.pl.dotplot, groupby='cell_type', title='dotplot', dendrogram=True + ), ), ( "dotplot2", @@ -415,7 +421,9 @@ def test_tracksplot(image_comparer): save_and_compare_images = image_comparer(ROOT, FIGS, tol=15) adata = sc.datasets.krumsiek11() - sc.pl.tracksplot(adata, adata.var_names, 'cell_type', dendrogram=True, use_raw=False) + sc.pl.tracksplot( + adata, adata.var_names, 'cell_type', dendrogram=True, use_raw=False + ) save_and_compare_images('master_tracksplot') @@ -429,8 +437,10 @@ def test_multiple_plots(image_comparer): 'B-cell': ['CD79A', 'CD79B', 'MS4A1'], 'myeloid': ['CST3', 'LYZ'], } - fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(20, 5), gridspec_kw={'wspace': 0.7}) - __ = sc.pl.stacked_violin( + fig, (ax1, ax2, ax3) = plt.subplots( + 1, 3, figsize=(20, 5), gridspec_kw={'wspace': 0.7} + ) + _ = sc.pl.stacked_violin( adata, markers, groupby='bulk_labels', @@ -439,7 +449,7 @@ def test_multiple_plots(image_comparer): dendrogram=True, show=False, ) - __ = sc.pl.dotplot( + _ = sc.pl.dotplot( adata, markers, groupby='bulk_labels', @@ -448,7 +458,7 @@ def test_multiple_plots(image_comparer): dendrogram=True, show=False, ) - __ = sc.pl.matrixplot( + _ = sc.pl.matrixplot( adata, markers, groupby='bulk_labels', @@ -550,7 +560,9 @@ def test_correlation(image_comparer): [ ( "ranked_genes_sharey", - partial(sc.pl.rank_genes_groups, n_genes=12, n_panels_per_row=3, show=False), + partial( + sc.pl.rank_genes_groups, n_genes=12, n_panels_per_row=3, show=False + ), ), ( "ranked_genes", @@ -564,7 +576,9 @@ def test_correlation(image_comparer): ), ( "ranked_genes_heatmap", - partial(sc.pl.rank_genes_groups_heatmap, n_genes=4, cmap='YlGnBu', show=False), + partial( + sc.pl.rank_genes_groups_heatmap, n_genes=4, cmap='YlGnBu', show=False + ), ), ( "ranked_genes_heatmap_swap_axes", @@ -741,7 +755,9 @@ def pbmc_scatterplots(): pytest.param( 'tsne', partial(sc.pl.tsne, color=['CD3D', 'louvain']), - marks=pytest.mark.xfail(reason='slight differences even after setting random_state.'), + marks=pytest.mark.xfail( + reason='slight differences even after setting random_state.' + ), ), ('umap_nocolor', sc.pl.umap), ( @@ -907,7 +923,10 @@ def test_scatter_rep(tmpdir): ), columns=["rep", "gene", "result"], ) - states["outpth"] = [TESTDIR / f"{state.gene}_{state.rep}_{state.result}.png" for state in states.itertuples()] + states["outpth"] = [ + TESTDIR / f"{state.gene}_{state.rep}_{state.result}.png" + for state in states.itertuples() + ] pattern = np.array(list(chain.from_iterable(repeat(i, 5) for i in range(3)))) coords = np.c_[np.arange(15) % 5, pattern] @@ -970,7 +989,10 @@ def test_paga(image_comparer): sc.pl.paga_compare(pbmc, basis='X_pca', legend_fontweight='normal', **common) save_and_compare_images('master_paga_compare_pca') - colors = {c: {cm.Set1(_): 0.33 for _ in range(3)} for c in pbmc.obs["bulk_labels"].cat.categories} + colors = { + c: {cm.Set1(_): 0.33 for _ in range(3)} + for c in pbmc.obs["bulk_labels"].cat.categories + } colors["Dendritic"] = {cm.Set2(_): 0.25 for _ in range(4)} sc.pl.paga(pbmc, color=colors, colorbar=False) diff --git a/scanpy/tests/test_preprocessing.py b/scanpy/tests/test_preprocessing.py index c2167c4796..b9decb0579 100644 --- a/scanpy/tests/test_preprocessing.py +++ b/scanpy/tests/test_preprocessing.py @@ -39,7 +39,9 @@ def base(request): def test_log1p_rep(count_matrix_format, base, dtype): - X = count_matrix_format(np.abs(sp.random(100, 200, density=0.3, dtype=dtype)).toarray()) + X = count_matrix_format( + np.abs(sp.random(100, 200, density=0.3, dtype=dtype)).toarray() + ) check_rep_mutation(sc.pp.log1p, X, base=base) check_rep_results(sc.pp.log1p, X, base=base) @@ -167,11 +169,15 @@ def test_regress_out_ordinal(): adata.obs['n_counts'] = adata.X.sum(axis=1) # results using only one processor - single = sc.pp.regress_out(adata, keys=['n_counts', 'percent_mito'], n_jobs=1, copy=True) + single = sc.pp.regress_out( + adata, keys=['n_counts', 'percent_mito'], n_jobs=1, copy=True + ) assert adata.X.shape == single.X.shape # results using 8 processors - multi = sc.pp.regress_out(adata, keys=['n_counts', 'percent_mito'], n_jobs=8, copy=True) + multi = sc.pp.regress_out( + adata, keys=['n_counts', 'percent_mito'], n_jobs=8, copy=True + ) np.testing.assert_array_equal(single.X, multi.X) @@ -249,11 +255,15 @@ def test_downsample_counts_per_cell(count_matrix_format, replace, dtype): X = X.astype(dtype) adata = AnnData(X=count_matrix_format(X), dtype=dtype) with pytest.raises(ValueError): - sc.pp.downsample_counts(adata, counts_per_cell=TARGET, total_counts=TARGET, replace=replace) + sc.pp.downsample_counts( + adata, counts_per_cell=TARGET, total_counts=TARGET, replace=replace + ) with pytest.raises(ValueError): sc.pp.downsample_counts(adata, replace=replace) initial_totals = np.ravel(adata.X.sum(axis=1)) - adata = sc.pp.downsample_counts(adata, counts_per_cell=TARGET, replace=replace, copy=True) + adata = sc.pp.downsample_counts( + adata, counts_per_cell=TARGET, replace=replace, copy=True + ) new_totals = np.ravel(adata.X.sum(axis=1)) if sp.issparse(adata.X): assert all(adata.X.toarray()[X == 0] == 0) @@ -261,13 +271,17 @@ def test_downsample_counts_per_cell(count_matrix_format, replace, dtype): assert all(adata.X[X == 0] == 0) assert all(new_totals <= TARGET) assert all(initial_totals >= new_totals) - assert all(initial_totals[initial_totals <= TARGET] == new_totals[initial_totals <= TARGET]) + assert all( + initial_totals[initial_totals <= TARGET] == new_totals[initial_totals <= TARGET] + ) if not replace: assert np.all(X >= adata.X) assert X.dtype == adata.X.dtype -def test_downsample_counts_per_cell_multiple_targets(count_matrix_format, replace, dtype): +def test_downsample_counts_per_cell_multiple_targets( + count_matrix_format, replace, dtype +): TARGETS = np.random.randint(500, 1500, 1000) X = np.random.randint(0, 100, (1000, 100)) * np.random.binomial(1, 0.3, (1000, 100)) X = X.astype(dtype) @@ -275,7 +289,9 @@ def test_downsample_counts_per_cell_multiple_targets(count_matrix_format, replac initial_totals = np.ravel(adata.X.sum(axis=1)) with pytest.raises(ValueError): sc.pp.downsample_counts(adata, counts_per_cell=[40, 10], replace=replace) - adata = sc.pp.downsample_counts(adata, counts_per_cell=TARGETS, replace=replace, copy=True) + adata = sc.pp.downsample_counts( + adata, counts_per_cell=TARGETS, replace=replace, copy=True + ) new_totals = np.ravel(adata.X.sum(axis=1)) if sp.issparse(adata.X): assert all(adata.X.toarray()[X == 0] == 0) @@ -283,7 +299,10 @@ def test_downsample_counts_per_cell_multiple_targets(count_matrix_format, replac assert all(adata.X[X == 0] == 0) assert all(new_totals <= TARGETS) assert all(initial_totals >= new_totals) - assert all(initial_totals[initial_totals <= TARGETS] == new_totals[initial_totals <= TARGETS]) + assert all( + initial_totals[initial_totals <= TARGETS] + == new_totals[initial_totals <= TARGETS] + ) if not replace: assert np.all(X >= adata.X) assert X.dtype == adata.X.dtype @@ -296,7 +315,9 @@ def test_downsample_total_counts(count_matrix_format, replace, dtype): total = X.sum() target = np.floor_divide(total, 10) initial_totals = np.ravel(adata_orig.X.sum(axis=1)) - adata = sc.pp.downsample_counts(adata_orig, total_counts=target, replace=replace, copy=True) + adata = sc.pp.downsample_counts( + adata_orig, total_counts=target, replace=replace, copy=True + ) new_totals = np.ravel(adata.X.sum(axis=1)) if sp.issparse(adata.X): assert all(adata.X.toarray()[X == 0] == 0) @@ -306,7 +327,9 @@ def test_downsample_total_counts(count_matrix_format, replace, dtype): assert all(initial_totals >= new_totals) if not replace: assert np.all(X >= adata.X) - adata = sc.pp.downsample_counts(adata_orig, total_counts=total + 10, replace=False, copy=True) + adata = sc.pp.downsample_counts( + adata_orig, total_counts=total + 10, replace=False, copy=True + ) assert (adata.X == X).all() assert X.dtype == adata.X.dtype diff --git a/scanpy/tests/test_preprocessing_distributed.py b/scanpy/tests/test_preprocessing_distributed.py index a550d90d7b..871656c1ce 100644 --- a/scanpy/tests/test_preprocessing_distributed.py +++ b/scanpy/tests/test_preprocessing_distributed.py @@ -5,9 +5,9 @@ import numpy.testing as npt import pytest -from scanpy.preprocessing import * -from scanpy.preprocessing._simple import materialize_as_ndarray - +from scanpy.preprocessing import normalize_total, filter_genes +from scanpy.preprocessing import log1p, normalize_per_cell, filter_cells +from scanpy.preprocessing._distributed import materialize_as_ndarray HERE = Path(__file__).parent / Path('_data/') input_file = str(Path(HERE, "10x-10k-subset.zarr")) @@ -16,7 +16,9 @@ installed = {mod: bool(find_spec(mod)) for mod in required} -@pytest.mark.skipif(not all(installed.values()), reason=f'{required} all required: {installed}') +@pytest.mark.skipif( + not all(installed.values()), reason=f'{required} all required: {installed}' +) class TestPreprocessingDistributed: @pytest.fixture() def adata(self): diff --git a/scanpy/tests/test_qc_metrics.py b/scanpy/tests/test_qc_metrics.py index 33869de5a1..71f6e728e0 100644 --- a/scanpy/tests/test_qc_metrics.py +++ b/scanpy/tests/test_qc_metrics.py @@ -50,7 +50,9 @@ def test_segments_binary(): assert (segfull == propfull).all() -@pytest.mark.parametrize("cls", [np.asarray, sparse.csr_matrix, sparse.csc_matrix, sparse.coo_matrix]) +@pytest.mark.parametrize( + "cls", [np.asarray, sparse.csr_matrix, sparse.csc_matrix, sparse.coo_matrix] +) def test_top_segments(cls): a = cls(np.ones((300, 100))) seg = top_segment_proportions(a, [50, 100]) @@ -65,16 +67,25 @@ def test_top_segments(cls): # they’re also just making sure the metrics are there def test_qc_metrics(): adata = AnnData(X=sparse.csr_matrix(np.random.binomial(100, 0.005, (1000, 1000)))) - adata.var["mito"] = np.concatenate((np.ones(100, dtype=bool), np.zeros(900, dtype=bool))) + adata.var["mito"] = np.concatenate( + (np.ones(100, dtype=bool), np.zeros(900, dtype=bool)) + ) adata.var["negative"] = False sc.pp.calculate_qc_metrics(adata, qc_vars=["mito", "negative"], inplace=True) assert (adata.obs["n_genes_by_counts"] < adata.shape[1]).all() - assert (adata.obs["n_genes_by_counts"] >= adata.obs["log1p_n_genes_by_counts"]).all() + assert ( + adata.obs["n_genes_by_counts"] >= adata.obs["log1p_n_genes_by_counts"] + ).all() assert (adata.obs["total_counts"] == np.ravel(adata.X.sum(axis=1))).all() assert (adata.obs["total_counts"] >= adata.obs["log1p_total_counts"]).all() - assert (adata.obs["total_counts_mito"] >= adata.obs["log1p_total_counts_mito"]).all() + assert ( + adata.obs["total_counts_mito"] >= adata.obs["log1p_total_counts_mito"] + ).all() assert (adata.obs["total_counts_negative"] == 0).all() - assert (adata.obs["pct_counts_in_top_50_genes"] <= adata.obs["pct_counts_in_top_100_genes"]).all() + assert ( + adata.obs["pct_counts_in_top_50_genes"] + <= adata.obs["pct_counts_in_top_100_genes"] + ).all() for col in filter(lambda x: "negative" not in x, adata.obs.columns): assert (adata.obs[col] >= 0).all() # Values should be positive or zero assert (adata.obs[col] != 0).any().all() # Nothing should be all zeros @@ -97,21 +108,29 @@ def test_qc_metrics(): assert np.allclose(adata.var[col], old_var[col]) # with log1p=False adata = AnnData(X=sparse.csr_matrix(np.random.binomial(100, 0.005, (1000, 1000)))) - adata.var["mito"] = np.concatenate((np.ones(100, dtype=bool), np.zeros(900, dtype=bool))) + adata.var["mito"] = np.concatenate( + (np.ones(100, dtype=bool), np.zeros(900, dtype=bool)) + ) adata.var["negative"] = False - sc.pp.calculate_qc_metrics(adata, qc_vars=["mito", "negative"], log1p=False, inplace=True) + sc.pp.calculate_qc_metrics( + adata, qc_vars=["mito", "negative"], log1p=False, inplace=True + ) assert not np.any(adata.obs.columns.str.startswith("log1p_")) assert not np.any(adata.var.columns.str.startswith("log1p_")) def adata_mito(): a = np.random.binomial(100, 0.005, (1000, 1000)) - init_var = pd.DataFrame(dict(mito=np.concatenate((np.ones(100, dtype=bool), np.zeros(900, dtype=bool))))) + init_var = pd.DataFrame( + dict(mito=np.concatenate((np.ones(100, dtype=bool), np.zeros(900, dtype=bool)))) + ) adata_dense = AnnData(X=a, var=init_var.copy()) return adata_dense, init_var -@pytest.mark.parametrize("cls", [np.asarray, sparse.csr_matrix, sparse.csc_matrix, sparse.coo_matrix]) +@pytest.mark.parametrize( + "cls", [np.asarray, sparse.csr_matrix, sparse.csc_matrix, sparse.coo_matrix] +) def test_qc_metrics_format(cls): adata_dense, init_var = adata_mito() sc.pp.calculate_qc_metrics(adata_dense, qc_vars=["mito"], inplace=True) diff --git a/scanpy/tests/test_queries.py b/scanpy/tests/test_queries.py index c97e2e6767..e4ef9cc69d 100644 --- a/scanpy/tests/test_queries.py +++ b/scanpy/tests/test_queries.py @@ -20,7 +20,9 @@ def test_enrich(): sc.queries.enrich(pbmc, "1") gene_dict = {'set1': ['KLF4', 'PAX5'], 'set2': ['SOX2', 'NANOG']} - enrich_list = sc.queries.enrich(gene_dict, org="hsapiens", gprofiler_kwargs=dict(sources=['GO:BP'])) + enrich_list = sc.queries.enrich( + gene_dict, org="hsapiens", gprofiler_kwargs=dict(sources=['GO:BP']) + ) assert 'set1' in enrich_list['query'].unique() assert 'set2' in enrich_list['query'].unique() @@ -29,4 +31,6 @@ def test_enrich(): def test_mito_genes(): pbmc = sc.datasets.pbmc68k_reduced() mt_genes = sc.queries.mitochondrial_genes("hsapiens") - assert pbmc.var_names.isin(mt_genes["external_gene_name"]).sum() == 1 # Should only be MT-ND3 + assert ( + pbmc.var_names.isin(mt_genes["external_gene_name"]).sum() == 1 + ) # Should only be MT-ND3 diff --git a/scanpy/tests/test_rank_genes_groups.py b/scanpy/tests/test_rank_genes_groups.py index 269adb4990..1592a5c6a8 100644 --- a/scanpy/tests/test_rank_genes_groups.py +++ b/scanpy/tests/test_rank_genes_groups.py @@ -28,9 +28,13 @@ def get_example_data(*, sparse=False): # create test object - adata = AnnData(np.multiply(binomial(1, 0.15, (100, 20)), negative_binomial(2, 0.25, (100, 20)))) + adata = AnnData( + np.multiply(binomial(1, 0.15, (100, 20)), negative_binomial(2, 0.25, (100, 20))) + ) # adapt marker_genes for cluster (so as to have some form of reasonable input - adata.X[0:10, 0:5] = np.multiply(binomial(1, 0.9, (10, 5)), negative_binomial(1, 0.5, (10, 5))) + adata.X[0:10, 0:5] = np.multiply( + binomial(1, 0.9, (10, 5)), negative_binomial(1, 0.5, (10, 5)) + ) # The following construction is inefficient, but makes sure that the same data is used in the sparse case if sparse: @@ -77,22 +81,30 @@ def test_results_dense(): rank_genes_groups(adata, 'true_groups', n_genes=20, method='t-test') - adata.uns['rank_genes_groups']['names'] = adata.uns['rank_genes_groups']['names'].astype(true_names_t_test.dtype) + adata.uns['rank_genes_groups']['names'] = adata.uns['rank_genes_groups'][ + 'names' + ].astype(true_names_t_test.dtype) for name in true_scores_t_test.dtype.names: - assert np.allclose(true_scores_t_test[name], adata.uns['rank_genes_groups']['scores'][name]) + assert np.allclose( + true_scores_t_test[name], adata.uns['rank_genes_groups']['scores'][name] + ) assert np.array_equal(true_names_t_test, adata.uns['rank_genes_groups']['names']) rank_genes_groups(adata, 'true_groups', n_genes=20, method='wilcoxon') - adata.uns['rank_genes_groups']['names'] = adata.uns['rank_genes_groups']['names'].astype(true_names_wilcoxon.dtype) + adata.uns['rank_genes_groups']['names'] = adata.uns['rank_genes_groups'][ + 'names' + ].astype(true_names_wilcoxon.dtype) for name in true_scores_t_test.dtype.names: assert np.allclose( true_scores_wilcoxon[name][:7], adata.uns['rank_genes_groups']['scores'][name][:7], ) - assert np.array_equal(true_names_wilcoxon[:7], adata.uns['rank_genes_groups']['names'][:7]) + assert np.array_equal( + true_names_wilcoxon[:7], adata.uns['rank_genes_groups']['names'][:7] + ) def test_results_sparse(): @@ -109,22 +121,30 @@ def test_results_sparse(): rank_genes_groups(adata, 'true_groups', n_genes=20, method='t-test') - adata.uns['rank_genes_groups']['names'] = adata.uns['rank_genes_groups']['names'].astype(true_names_t_test.dtype) + adata.uns['rank_genes_groups']['names'] = adata.uns['rank_genes_groups'][ + 'names' + ].astype(true_names_t_test.dtype) for name in true_scores_t_test.dtype.names: - assert np.allclose(true_scores_t_test[name], adata.uns['rank_genes_groups']['scores'][name]) + assert np.allclose( + true_scores_t_test[name], adata.uns['rank_genes_groups']['scores'][name] + ) assert np.array_equal(true_names_t_test, adata.uns['rank_genes_groups']['names']) rank_genes_groups(adata, 'true_groups', n_genes=20, method='wilcoxon') - adata.uns['rank_genes_groups']['names'] = adata.uns['rank_genes_groups']['names'].astype(true_names_wilcoxon.dtype) + adata.uns['rank_genes_groups']['names'] = adata.uns['rank_genes_groups'][ + 'names' + ].astype(true_names_wilcoxon.dtype) for name in true_scores_t_test.dtype.names: assert np.allclose( true_scores_wilcoxon[name][:7], adata.uns['rank_genes_groups']['scores'][name][:7], ) - assert np.array_equal(true_names_wilcoxon[:7], adata.uns['rank_genes_groups']['names'][:7]) + assert np.array_equal( + true_names_wilcoxon[:7], adata.uns['rank_genes_groups']['names'][:7] + ) def test_results_layers(): @@ -215,7 +235,11 @@ def test_wilcoxon_symmetry(): rankby_abs=True, ) - stats_mono = rank_genes_groups_df(pbmc, group="CD14+ Monocyte").drop(columns="names").to_numpy() + stats_mono = ( + rank_genes_groups_df(pbmc, group="CD14+ Monocyte") + .drop(columns="names") + .to_numpy() + ) rank_genes_groups( pbmc, @@ -226,7 +250,9 @@ def test_wilcoxon_symmetry(): rankby_abs=True, ) - stats_dend = rank_genes_groups_df(pbmc, group="Dendritic").drop(columns="names").to_numpy() + stats_dend = ( + rank_genes_groups_df(pbmc, group="Dendritic").drop(columns="names").to_numpy() + ) assert np.allclose(np.abs(stats_mono), np.abs(stats_dend)) diff --git a/scanpy/tests/test_rank_genes_groups_logreg.py b/scanpy/tests/test_rank_genes_groups_logreg.py index a13997458e..f64a3c3fb7 100644 --- a/scanpy/tests/test_rank_genes_groups_logreg.py +++ b/scanpy/tests/test_rank_genes_groups_logreg.py @@ -34,7 +34,9 @@ def test_rank_genes_groups_with_renamed_categories_use_rep(): adata.X = adata.X[::-1, :] sc.tl.louvain(adata) - sc.tl.rank_genes_groups(adata, 'louvain', method='logreg', layer="to_test", use_raw=False) + sc.tl.rank_genes_groups( + adata, 'louvain', method='logreg', layer="to_test", use_raw=False + ) assert adata.uns['rank_genes_groups']['names'].dtype.names == ('0', '1', '2') assert adata.uns['rank_genes_groups']['names'][0].tolist() == ('3', '1', '0') diff --git a/scanpy/tests/test_read_10x.py b/scanpy/tests/test_read_10x.py index 0ee8d7806c..6ed125ab4c 100644 --- a/scanpy/tests/test_read_10x.py +++ b/scanpy/tests/test_read_10x.py @@ -67,7 +67,9 @@ def test_read_10x_h5_v1(): ROOT / '1.2.0' / 'filtered_gene_bc_matrices_h5.h5', genome='hg19_chr21', ) - nospec_genome_v1 = sc.read_10x_h5(ROOT / '1.2.0' / 'filtered_gene_bc_matrices_h5.h5') + nospec_genome_v1 = sc.read_10x_h5( + ROOT / '1.2.0' / 'filtered_gene_bc_matrices_h5.h5' + ) assert_anndata_equal(spec_genome_v1, nospec_genome_v1) @@ -111,4 +113,6 @@ def test_read_visium_counts(): def test_10x_h5_gex(): # Tests that gex option doesn't, say, make the function return None h5_pth = ROOT / '3.0.0' / 'filtered_feature_bc_matrix.h5' - assert_anndata_equal(sc.read_10x_h5(h5_pth, gex_only=True), sc.read_10x_h5(h5_pth, gex_only=False)) + assert_anndata_equal( + sc.read_10x_h5(h5_pth, gex_only=True), sc.read_10x_h5(h5_pth, gex_only=False) + ) diff --git a/scanpy/tests/test_score_genes.py b/scanpy/tests/test_score_genes.py index 497a72e0fc..9500031f66 100644 --- a/scanpy/tests/test_score_genes.py +++ b/scanpy/tests/test_score_genes.py @@ -9,7 +9,12 @@ def _create_random_gene_names(n_genes, name_length): """ creates a bunch of random gene names (just CAPS letters) """ - return np.array([''.join(map(chr, np.random.randint(65, 90, name_length))) for _ in range(n_genes)]) + return np.array( + [ + ''.join(map(chr, np.random.randint(65, 90, name_length))) + for _ in range(n_genes) + ] + ) def _create_sparse_nan_matrix(rows, cols, percent_zero, percent_nan): @@ -52,7 +57,9 @@ def test_add_score(): # the actual genes names are all 6letters # create some non-estinsting names with 7 letters: non_existing_genes = _create_random_gene_names(n_genes=3, name_length=7) - some_genes = np.r_[np.unique(np.random.choice(adata.var_names, 10)), np.unique(non_existing_genes)] + some_genes = np.r_[ + np.unique(np.random.choice(adata.var_names, 10)), np.unique(non_existing_genes) + ] sc.tl.score_genes(adata, some_genes, score_name='Test') assert adata.obs['Test'].dtype == 'float32' @@ -73,8 +80,12 @@ def test_sparse_nanmean(): # sparse matrix with nan S = _create_sparse_nan_matrix(R, C, percent_zero=0.3, percent_nan=0.3) - np.testing.assert_allclose(np.nanmean(S.A, 1), np.array(_sparse_nanmean(S, 1)).flatten()) - np.testing.assert_allclose(np.nanmean(S.A, 0), np.array(_sparse_nanmean(S, 0)).flatten()) + np.testing.assert_allclose( + np.nanmean(S.A, 1), np.array(_sparse_nanmean(S, 1)).flatten() + ) + np.testing.assert_allclose( + np.nanmean(S.A, 0), np.array(_sparse_nanmean(S, 0)).flatten() + ) # edge case of only NaNs per row A = np.full((10, 1), np.nan) @@ -107,7 +118,9 @@ def test_score_genes_sparse_vs_dense(): sc.tl.score_genes(adata_sparse, gene_list=gene_set, score_name='Test') sc.tl.score_genes(adata_dense, gene_list=gene_set, score_name='Test') - np.testing.assert_allclose(adata_sparse.obs['Test'].values, adata_dense.obs['Test'].values) + np.testing.assert_allclose( + adata_sparse.obs['Test'].values, adata_dense.obs['Test'].values + ) def test_score_genes_deplete(): diff --git a/scanpy/tools/_dendrogram.py b/scanpy/tools/_dendrogram.py index 7bcd67328c..788db127a6 100644 --- a/scanpy/tools/_dendrogram.py +++ b/scanpy/tools/_dendrogram.py @@ -109,12 +109,16 @@ def dendrogram( ) if var_names is None: - rep_df = pd.DataFrame(_choose_representation(adata, use_rep=use_rep, n_pcs=n_pcs)) + rep_df = pd.DataFrame( + _choose_representation(adata, use_rep=use_rep, n_pcs=n_pcs) + ) categorical = adata.obs[groupby[0]] if len(groupby) > 1: for group in groupby[1:]: # create new category by merging the given groupby categories - categorical = (categorical.astype(str) + "_" + adata.obs[group].astype(str)).astype('category') + categorical = ( + categorical.astype(str) + "_" + adata.obs[group].astype(str) + ).astype('category') categorical.name = "_".join(groupby) rep_df.set_index(categorical, inplace=True) @@ -133,7 +137,9 @@ def dendrogram( corr_matrix = mean_df.T.corr(method=cor_method) corr_condensed = distance.squareform(1 - corr_matrix) - z_var = sch.linkage(corr_condensed, method=linkage_method, optimal_ordering=optimal_ordering) + z_var = sch.linkage( + corr_condensed, method=linkage_method, optimal_ordering=optimal_ordering + ) dendro_info = sch.dendrogram(z_var, labels=list(categories), no_plot=True) dat = dict( diff --git a/scanpy/tools/_diffmap.py b/scanpy/tools/_diffmap.py index a04e3b4a1a..e5b5b8b8f4 100644 --- a/scanpy/tools/_diffmap.py +++ b/scanpy/tools/_diffmap.py @@ -64,7 +64,9 @@ def diffmap( neighbors_key = 'neighbors' if neighbors_key not in adata.uns: - raise ValueError('You need to run `pp.neighbors` first to compute a neighborhood graph.') + raise ValueError( + 'You need to run `pp.neighbors` first to compute a neighborhood graph.' + ) if n_comps <= 2: raise ValueError('Provide any value greater than 2 for `n_comps`. ') adata = adata.copy() if copy else adata diff --git a/scanpy/tools/_dpt.py b/scanpy/tools/_dpt.py index 4529cecda9..35937dc7f0 100644 --- a/scanpy/tools/_dpt.py +++ b/scanpy/tools/_dpt.py @@ -148,7 +148,9 @@ def dpt( logg.info(' this uses a hierarchical implementation') if dpt.iroot is not None: dpt._set_pseudotime() # pseudotimes are distances from root point - adata.uns['iroot'] = dpt.iroot # update iroot, might have changed when subsampling, for example + adata.uns[ + 'iroot' + ] = dpt.iroot # update iroot, might have changed when subsampling, for example adata.obs['dpt_pseudotime'] = dpt.pseudotime # detect branchings and partition the data into segments if n_branchings > 0: @@ -172,7 +174,11 @@ def dpt( time=start, deep=( 'added\n' - + (" 'dpt_pseudotime', the pseudotime (adata.obs)" if dpt.iroot is not None else '') + + ( + " 'dpt_pseudotime', the pseudotime (adata.obs)" + if dpt.iroot is not None + else '' + ) + ( "\n 'dpt_groups', the branching subgroups of dpt (adata.obs)" "\n 'dpt_order', cell order (adata.obs)" @@ -201,7 +207,11 @@ def __init__( super().__init__(adata, n_dcs=n_dcs, neighbors_key=neighbors_key) self.flavor = 'haghverdi16' self.n_branchings = n_branchings - self.min_group_size = min_group_size if min_group_size >= 1 else int(min_group_size * self._adata.shape[0]) + self.min_group_size = ( + min_group_size + if min_group_size >= 1 + else int(min_group_size * self._adata.shape[0]) + ) self.passed_adata = adata # just for debugging purposes self.choose_largest_segment = False self.allow_kendall_tau_shift = allow_kendall_tau_shift @@ -241,7 +251,8 @@ def detect_branchings(self): List of indices of the tips of segments. """ logg.debug( - f' detect {self.n_branchings} ' f'branching{"" if self.n_branchings == 1 else "s"}', + f' detect {self.n_branchings} ' + f'branching{"" if self.n_branchings == 1 else "s"}', ) # a segment is a subset of points of the data set (defined by the # indices of the points in the segment) @@ -271,7 +282,11 @@ def detect_branchings(self): # # let us define the tips of the whole data set if False: # this is safe, but not compatible with on-the-fly computation - tips_all = np.array(np.unravel_index(np.argmax(self.distances_dpt), self.distances_dpt.shape)) + tips_all = np.array( + np.unravel_index( + np.argmax(self.distances_dpt), self.distances_dpt.shape + ) + ) else: if self.iroot is not None: tip_0 = np.argmax(self.distances_dpt[self.iroot]) @@ -283,7 +298,10 @@ def detect_branchings(self): segs_connects = [[]] segs_undecided = [True] segs_adjacency = [[]] - logg.debug(' do not consider groups with less than ' f'{self.min_group_size} points for splitting') + logg.debug( + ' do not consider groups with less than ' + f'{self.min_group_size} points for splitting' + ) for ibranch in range(self.n_branchings): iseg, tips3 = self.select_segment(segs, segs_tips, segs_undecided) if iseg == -1: @@ -313,7 +331,9 @@ def detect_branchings(self): self.segs_connects[i, seg_adjacency] = segs_connects[i] for i in range(len(segs)): for j in range(len(segs)): - self.segs_adjacency[i, j] = self.distances_dpt[self.segs_connects[i, j], self.segs_connects[j, i]] + self.segs_adjacency[i, j] = self.distances_dpt[ + self.segs_connects[i, j], self.segs_connects[j, i] + ] self.segs_adjacency = self.segs_adjacency.tocsr() self.segs_connects = self.segs_connects.tocsr() @@ -324,11 +344,13 @@ def check_adjacency(self): if n_edges_per_seg[iseg] == n_edges: _ = self.segs_adjacency[iseg].todense().A1 closest_points_other_segs = [ - seg[np.argmin(self.distances_dpt[self.segs_tips[iseg][0], seg])] for seg in self.segs + seg[np.argmin(self.distances_dpt[self.segs_tips[iseg][0], seg])] + for seg in self.segs ] seg = self.segs[iseg] closest_points_in_segs = [ - seg[np.argmin(self.distances_dpt[tips[0], seg])] for tips in self.segs_tips + seg[np.argmin(self.distances_dpt[tips[0], seg])] + for tips in self.segs_tips ] distance_segs = [ self.distances_dpt[closest_points_other_segs[ipoint], point] @@ -381,8 +403,13 @@ def select_segment(self, segs, segs_tips, segs_undecided) -> Tuple[int, int]: # take the inner tip, the "second tip" of the segment for itip in range(2): if ( - self.distances_dpt[segs_tips[jseg][1], segs_tips[iseg][itip]] - < 0.5 * self.distances_dpt[segs_tips[iseg][~itip], segs_tips[iseg][itip]] + self.distances_dpt[ + segs_tips[jseg][1], segs_tips[iseg][itip] + ] + < 0.5 + * self.distances_dpt[ + segs_tips[iseg][~itip], segs_tips[iseg][itip] + ] ): # logg.debug( # ' group', iseg, 'with tip', segs_tips[iseg][itip], @@ -416,7 +443,9 @@ def select_segment(self, segs, segs_tips, segs_undecided) -> Tuple[int, int]: # if we did not normalize, there would be a danger of simply # assigning the highest score to the longest segment score = dseg[tips3[2]] / Dseg[tips3[0], tips3[1]] - score = len(seg) if self.choose_largest_segment else score # simply the number of points + score = ( + len(seg) if self.choose_largest_segment else score + ) # simply the number of points logg.debug( f' group {iseg} score {score} n_points {len(seg)} ' + '(too small)' if len(seg) < self.min_group_size @@ -547,7 +576,9 @@ def detect_branching( segs_tips.insert(iseg, ssegs_tips[trunk]) # append other segments segs += [seg for iseg, seg in enumerate(ssegs) if iseg != trunk] - segs_tips += [seg_tips for iseg, seg_tips in enumerate(ssegs_tips) if iseg != trunk] + segs_tips += [ + seg_tips for iseg, seg_tips in enumerate(ssegs_tips) if iseg != trunk + ] if len(ssegs) == 4: # insert undecided cells at same position segs_undecided.pop(iseg) @@ -557,7 +588,11 @@ def detect_branching( prev_connecting_segments = segs_adjacency[iseg].copy() if self.flavor == 'haghverdi16': segs_adjacency += [[iseg] for i in range(n_add)] - segs_connects += [seg_connects for iseg, seg_connects in enumerate(ssegs_connects) if iseg != trunk] + segs_connects += [ + seg_connects + for iseg, seg_connects in enumerate(ssegs_connects) + if iseg != trunk + ] _ = segs_connects[iseg] for jseg_cnt, jseg in enumerate(prev_connecting_segments): iseg_cnt = 0 @@ -575,7 +610,9 @@ def detect_branching( segs_connects[kseg].append(idx) break iseg_cnt += 1 - segs_adjacency[iseg] += list(range(len(segs_adjacency) - n_add, len(segs_adjacency))) + segs_adjacency[iseg] += list( + range(len(segs_adjacency) - n_add, len(segs_adjacency)) + ) segs_connects[iseg] += ssegs_connects[trunk] else: import networkx as nx @@ -591,14 +628,28 @@ def detect_branching( for kseg in kseg_list: reference_point_in_k = segs_tips[kseg][0] closest_points_in_jseg.append( - segs[jseg][np.argmin(self.distances_dpt[reference_point_in_k, segs[jseg]])] + segs[jseg][ + np.argmin( + self.distances_dpt[reference_point_in_k, segs[jseg]] + ) + ] ) # do not use the tip in the large segment j, instead, use the closest point - reference_point_in_j = closest_points_in_jseg[-1] # segs_tips[jseg][0] + reference_point_in_j = closest_points_in_jseg[ + -1 + ] # segs_tips[jseg][0] closest_points_in_kseg.append( - segs[kseg][np.argmin(self.distances_dpt[reference_point_in_j, segs[kseg]])] + segs[kseg][ + np.argmin( + self.distances_dpt[reference_point_in_j, segs[kseg]] + ) + ] + ) + distances.append( + self.distances_dpt[ + closest_points_in_jseg[-1], closest_points_in_kseg[-1] + ] ) - distances.append(self.distances_dpt[closest_points_in_jseg[-1], closest_points_in_kseg[-1]]) # print(jseg, '(', segs_tips[jseg][0], closest_points_in_jseg[-1], ')', # kseg, '(', segs_tips[kseg][0], closest_points_in_kseg[-1], ') :', distances[-1]) idx = np.argmin(distances) @@ -619,22 +670,42 @@ def detect_branching( distances = [] closest_points_in_jseg = [] closest_points_in_kseg = [] - jseg_list = [jseg for jseg in range(len(segs)) if jseg != kseg and jseg not in prev_connecting_segments] + jseg_list = [ + jseg + for jseg in range(len(segs)) + if jseg != kseg and jseg not in prev_connecting_segments + ] for jseg in jseg_list: reference_point_in_k = segs_tips[kseg][0] closest_points_in_jseg.append( - segs[jseg][np.argmin(self.distances_dpt[reference_point_in_k, segs[jseg]])] + segs[jseg][ + np.argmin( + self.distances_dpt[reference_point_in_k, segs[jseg]] + ) + ] ) # do not use the tip in the large segment j, instead, use the closest point - reference_point_in_j = closest_points_in_jseg[-1] # segs_tips[jseg][0] + reference_point_in_j = closest_points_in_jseg[ + -1 + ] # segs_tips[jseg][0] closest_points_in_kseg.append( - segs[kseg][np.argmin(self.distances_dpt[reference_point_in_j, segs[kseg]])] + segs[kseg][ + np.argmin( + self.distances_dpt[reference_point_in_j, segs[kseg]] + ) + ] + ) + distances.append( + self.distances_dpt[ + closest_points_in_jseg[-1], closest_points_in_kseg[-1] + ] ) - distances.append(self.distances_dpt[closest_points_in_jseg[-1], closest_points_in_kseg[-1]]) idx = np.argmin(distances) jseg_min = jseg_list[idx] if jseg_min not in kseg_list: - segs_adjacency_sparse = sp.sparse.lil_matrix((len(segs), len(segs)), dtype=float) + segs_adjacency_sparse = sp.sparse.lil_matrix( + (len(segs), len(segs)), dtype=float + ) for i, seg_adjacency in enumerate(segs_adjacency): segs_adjacency_sparse[i, seg_adjacency] = 1 G = nx.Graph(segs_adjacency_sparse) @@ -648,7 +719,10 @@ def detect_branching( # if we split the cluster, we should not attach kseg do_not_attach_kseg = True else: - logg.debug(f' cannot attach new segment {kseg} at {jseg_min} ' '(would produce cycle)') + logg.debug( + f' cannot attach new segment {kseg} at {jseg_min} ' + '(would produce cycle)' + ) if kseg != kseg_list[-1]: logg.debug(' continue') continue @@ -668,7 +742,9 @@ def _detect_branching( Dseg: np.ndarray, tips: np.ndarray, seg_reference=None, - ) -> Tuple[List[np.ndarray], List[np.ndarray], List[List[int]], List[List[int]], int]: + ) -> Tuple[ + List[np.ndarray], List[np.ndarray], List[List[int]], List[List[int]], int + ]: """\ Detect branching on given segment. @@ -705,7 +781,9 @@ def _detect_branching( elif self.flavor == 'wolf17_bi' or self.flavor == 'wolf17_bi_un': ssegs = self._detect_branching_single_wolf17_bi(Dseg, tips) else: - raise ValueError('`flavor` needs to be in {"haghverdi16", "wolf17_tri", "wolf17_bi"}.') + raise ValueError( + '`flavor` needs to be in {"haghverdi16", "wolf17_tri", "wolf17_bi"}.' + ) # make sure that each data point has a unique association with a segment masks = np.zeros((len(ssegs), Dseg.shape[0]), dtype=bool) for iseg, seg in enumerate(ssegs): @@ -730,13 +808,19 @@ def _detect_branching( for inewseg, newseg_tips in enumerate(ssegs_tips): reference_point = newseg_tips[0] # closest cell to the new segment within undecided cells - closest_cell = undecided_cells[np.argmin(Dseg[reference_point][undecided_cells])] + closest_cell = undecided_cells[ + np.argmin(Dseg[reference_point][undecided_cells]) + ] ssegs_connects[inewseg].append(closest_cell) # closest cell to the undecided cells within new segment - closest_cell = ssegs[inewseg][np.argmin(Dseg[closest_cell][ssegs[inewseg]])] + closest_cell = ssegs[inewseg][ + np.argmin(Dseg[closest_cell][ssegs[inewseg]]) + ] ssegs_connects[-1].append(closest_cell) # also compute tips for the undecided cells - tip_0 = undecided_cells[np.argmax(Dseg[undecided_cells[0]][undecided_cells])] + tip_0 = undecided_cells[ + np.argmax(Dseg[undecided_cells[0]][undecided_cells]) + ] tip_1 = undecided_cells[np.argmax(Dseg[tip_0][undecided_cells])] ssegs_tips.append([tip_0, tip_1]) ssegs_adjacency = [[3], [3], [3], [0, 1, 2]] @@ -750,35 +834,59 @@ def _detect_branching( # this is another strategy than for the undecided_cells # here it's possible to use the more symmetric procedure # shouldn't make much of a difference - closest_points[0, 1] = ssegs[1][np.argmin(Dseg[reference_point[0]][ssegs[1]])] - closest_points[1, 0] = ssegs[0][np.argmin(Dseg[reference_point[1]][ssegs[0]])] - closest_points[0, 2] = ssegs[2][np.argmin(Dseg[reference_point[0]][ssegs[2]])] - closest_points[2, 0] = ssegs[0][np.argmin(Dseg[reference_point[2]][ssegs[0]])] - closest_points[1, 2] = ssegs[2][np.argmin(Dseg[reference_point[1]][ssegs[2]])] - closest_points[2, 1] = ssegs[1][np.argmin(Dseg[reference_point[2]][ssegs[1]])] + closest_points[0, 1] = ssegs[1][ + np.argmin(Dseg[reference_point[0]][ssegs[1]]) + ] + closest_points[1, 0] = ssegs[0][ + np.argmin(Dseg[reference_point[1]][ssegs[0]]) + ] + closest_points[0, 2] = ssegs[2][ + np.argmin(Dseg[reference_point[0]][ssegs[2]]) + ] + closest_points[2, 0] = ssegs[0][ + np.argmin(Dseg[reference_point[2]][ssegs[0]]) + ] + closest_points[1, 2] = ssegs[2][ + np.argmin(Dseg[reference_point[1]][ssegs[2]]) + ] + closest_points[2, 1] = ssegs[1][ + np.argmin(Dseg[reference_point[2]][ssegs[1]]) + ] added_dist = np.zeros(3) added_dist[0] = ( - Dseg[closest_points[1, 0], closest_points[0, 1]] + Dseg[closest_points[2, 0], closest_points[0, 2]] + Dseg[closest_points[1, 0], closest_points[0, 1]] + + Dseg[closest_points[2, 0], closest_points[0, 2]] ) added_dist[1] = ( - Dseg[closest_points[0, 1], closest_points[1, 0]] + Dseg[closest_points[2, 1], closest_points[1, 2]] + Dseg[closest_points[0, 1], closest_points[1, 0]] + + Dseg[closest_points[2, 1], closest_points[1, 2]] ) added_dist[2] = ( - Dseg[closest_points[1, 2], closest_points[2, 1]] + Dseg[closest_points[0, 2], closest_points[2, 0]] + Dseg[closest_points[1, 2], closest_points[2, 1]] + + Dseg[closest_points[0, 2], closest_points[2, 0]] ) trunk = np.argmin(added_dist) - ssegs_adjacency = [[trunk] if i != trunk else [j for j in range(3) if j != trunk] for i in range(3)] + ssegs_adjacency = [ + [trunk] if i != trunk else [j for j in range(3) if j != trunk] + for i in range(3) + ] ssegs_connects = [ - [closest_points[i, trunk]] if i != trunk else [closest_points[trunk, j] for j in range(3) if j != trunk] + [closest_points[i, trunk]] + if i != trunk + else [closest_points[trunk, j] for j in range(3) if j != trunk] for i in range(3) ] else: trunk = 0 ssegs_adjacency = [[1], [0]] reference_point_in_0 = ssegs_tips[0][0] - closest_point_in_1 = ssegs[1][np.argmin(Dseg[reference_point_in_0][ssegs[1]])] + closest_point_in_1 = ssegs[1][ + np.argmin(Dseg[reference_point_in_0][ssegs[1]]) + ] reference_point_in_1 = closest_point_in_1 # ssegs_tips[1][0] - closest_point_in_0 = ssegs[0][np.argmin(Dseg[reference_point_in_1][ssegs[0]])] + closest_point_in_0 = ssegs[0][ + np.argmin(Dseg[reference_point_in_1][ssegs[0]]) + ] ssegs_connects = [[closest_point_in_1], [closest_point_in_0]] return ssegs, ssegs_tips, ssegs_adjacency, ssegs_connects, trunk @@ -828,7 +936,9 @@ def _detect_branching_single_wolf17_bi(self, Dseg, tips): ssegs = [closer_to_0_than_to_1, ~closer_to_0_than_to_1] return ssegs - def __detect_branching_haghverdi16(self, Dseg: np.ndarray, tips: np.ndarray) -> np.ndarray: + def __detect_branching_haghverdi16( + self, Dseg: np.ndarray, tips: np.ndarray + ) -> np.ndarray: """\ Detect branching on given segment. @@ -866,7 +976,9 @@ def __detect_branching_haghverdi16(self, Dseg: np.ndarray, tips: np.ndarray) -> # highly different, one would need to write the following equation # in terms of an ordering, such as exploited by the kendall # correlation method above - imax = np.argmin(Dseg[tips[0]][idcs] + Dseg[tips[1]][idcs] + Dseg[tips[2]][idcs]) + imax = np.argmin( + Dseg[tips[0]][idcs] + Dseg[tips[1]][idcs] + Dseg[tips[2]][idcs] + ) # init list to store new segments ssegs = [] # noqa: F841 # first new segment: all points until, but excluding the branching point diff --git a/scanpy/tools/_draw_graph.py b/scanpy/tools/_draw_graph.py index a8c55f3fa7..0447880388 100644 --- a/scanpy/tools/_draw_graph.py +++ b/scanpy/tools/_draw_graph.py @@ -156,7 +156,9 @@ def draw_graph( iterations = kwds['iterations'] else: iterations = 500 - positions = forceatlas2.forceatlas2(adjacency, pos=init_coords, iterations=iterations) + positions = forceatlas2.forceatlas2( + adjacency, pos=init_coords, iterations=iterations + ) positions = np.array(positions) else: # igraph doesn't use numpy seed diff --git a/scanpy/tools/_embedding_density.py b/scanpy/tools/_embedding_density.py index 64c9794db6..b946837f2c 100644 --- a/scanpy/tools/_embedding_density.py +++ b/scanpy/tools/_embedding_density.py @@ -119,7 +119,8 @@ def embedding_density( if f'X_{basis}' not in adata.obsm_keys(): raise ValueError( - "Cannot find the embedded representation " f"`adata.obsm['X_{basis}']`. Compute the embedding first." + "Cannot find the embedded representation " + f"`adata.obsm['X_{basis}']`. Compute the embedding first." ) if components is None: @@ -180,7 +181,9 @@ def embedding_density( if basis != 'diffmap': components += 1 - adata.uns[f'{density_covariate}_params'] = dict(covariate=groupby, components=components.tolist()) + adata.uns[f'{density_covariate}_params'] = dict( + covariate=groupby, components=components.tolist() + ) logg.hint( f"added\n" diff --git a/scanpy/tools/_ingest.py b/scanpy/tools/_ingest.py index ed09731c48..0941ded814 100644 --- a/scanpy/tools/_ingest.py +++ b/scanpy/tools/_ingest.py @@ -113,8 +113,12 @@ def ingest( start = logg.info('running ingest') obs = [obs] if isinstance(obs, str) else obs - embedding_method = [embedding_method] if isinstance(embedding_method, str) else embedding_method - labeling_method = [labeling_method] if isinstance(labeling_method, str) else labeling_method + embedding_method = ( + [embedding_method] if isinstance(embedding_method, str) else embedding_method + ) + labeling_method = ( + [labeling_method] if isinstance(labeling_method, str) else labeling_method + ) if len(labeling_method) == 1 and len(obs or []) > 1: labeling_method = labeling_method * len(obs) @@ -247,7 +251,9 @@ def _init_dist_search(self, dist_args): make_initialized_nnd_search, ) - self._random_init, self._tree_init = make_initialisations(dist_func, dist_args) + self._random_init, self._tree_init = make_initialisations( + dist_func, dist_args + ) _initialise_search = partial( initialise_search, init_from_random=self._random_init, @@ -379,7 +385,8 @@ def __init__(self, adata, neighbors_key=None): self._init_neighbors(adata, neighbors_key) else: raise ValueError( - f'There is no neighbors data in `adata.uns["{neighbors_key}"]`.\n' 'Please run pp.neighbors.' + f'There is no neighbors data in `adata.uns["{neighbors_key}"]`.\n' + 'Please run pp.neighbors.' ) if 'X_umap' in adata.obsm: @@ -429,7 +436,10 @@ def fit(self, adata_new): new_var_names = adata_new.var_names.str.upper() if not ref_var_names.equals(new_var_names): - raise ValueError('Variables in the new adata are different ' 'from variables in the reference adata') + raise ValueError( + 'Variables in the new adata are different ' + 'from variables in the reference adata' + ) self._obs = pd.DataFrame(index=adata_new.obs.index) self._obsm = _DimDict(adata_new.n_obs, axis=0) @@ -463,9 +473,13 @@ def neighbors(self, k=None, queue_size=5, epsilon=0.1, random_state=0): else: from umap.utils import deheap_sort - init = self._initialise_search(self._rp_forest, train, test, int(k * queue_size), rng_state=rng_state) + init = self._initialise_search( + self._rp_forest, train, test, int(k * queue_size), rng_state=rng_state + ) - result = self._search(train, self._search_graph.indptr, self._search_graph.indices, init, test) + result = self._search( + train, self._search_graph.indptr, self._search_graph.indices, init, test + ) indices, dists = deheap_sort(result) self._indices, self._distances = indices[:, :k], dists[:, :k] @@ -485,10 +499,14 @@ def map_embedding(self, method): elif method == 'pca': self._obsm['X_pca'] = self._pca() else: - raise NotImplementedError('Ingest supports only umap and pca embeddings for now.') + raise NotImplementedError( + 'Ingest supports only umap and pca embeddings for now.' + ) def _knn_classify(self, labels): - cat_array = self._adata_ref.obs[labels].astype('category') # ensure it's categorical + cat_array = self._adata_ref.obs[labels].astype( + 'category' + ) # ensure it's categorical values = [cat_array[inds].mode()[0] for inds in self._indices] return pd.Categorical(values=values, categories=cat_array.cat.categories) @@ -524,7 +542,9 @@ def to_adata(self, inplace=False): if not inplace: return adata - def to_adata_joint(self, batch_key='batch', batch_categories=None, index_unique='-'): + def to_adata_joint( + self, batch_key='batch', batch_categories=None, index_unique='-' + ): """\ Returns concatenated object. @@ -545,10 +565,14 @@ def to_adata_joint(self, batch_key='batch', batch_categories=None, index_unique= for key in self._obsm: if key in self._adata_ref.obsm: - adata.obsm[key] = np.vstack((self._adata_ref.obsm[key], self._obsm[key])) + adata.obsm[key] = np.vstack( + (self._adata_ref.obsm[key], self._obsm[key]) + ) if self._use_rep not in ('X_pca', 'X'): - adata.obsm[self._use_rep] = np.vstack((self._adata_ref.obsm[self._use_rep], self._obsm['rep'])) + adata.obsm[self._use_rep] = np.vstack( + (self._adata_ref.obsm[self._use_rep], self._obsm['rep']) + ) if 'X_umap' in self._obsm: adata.uns['umap'] = self._adata_ref.uns['umap'] diff --git a/scanpy/tools/_louvain.py b/scanpy/tools/_louvain.py index d8597a7588..9b09740284 100644 --- a/scanpy/tools/_louvain.py +++ b/scanpy/tools/_louvain.py @@ -108,7 +108,9 @@ def louvain( partition_kwargs = dict(partition_kwargs) start = logg.info('running Louvain clustering') if (flavor != 'vtraag') and (partition_type is not None): - raise ValueError('`partition_type` is only a valid argument ' 'when `flavour` is "vtraag"') + raise ValueError( + '`partition_type` is only a valid argument ' 'when `flavour` is "vtraag"' + ) adata = adata.copy() if copy else adata if adjacency is None: adjacency = _choose_graph(adata, obsp, neighbors_key) @@ -180,7 +182,12 @@ def louvain( logg.info(' using the "louvain" package of rapids') louvain_parts, _ = cugraph.louvain(g) - groups = louvain_parts.to_pandas().sort_values('vertex')[['partition']].to_numpy().ravel() + groups = ( + louvain_parts.to_pandas() + .sort_values('vertex')[['partition']] + .to_numpy() + .ravel() + ) elif flavor == 'taynaud': # this is deprecated import networkx as nx diff --git a/scanpy/tools/_marker_gene_overlap.py b/scanpy/tools/_marker_gene_overlap.py index 31b1d176dd..d50932216a 100644 --- a/scanpy/tools/_marker_gene_overlap.py +++ b/scanpy/tools/_marker_gene_overlap.py @@ -21,7 +21,10 @@ def _calc_overlap_count(markers1: dict, markers2: dict): overlaps = np.zeros((len(markers1), len(markers2))) for j, marker_group in enumerate(markers1): - tmp = [len(markers2[i].intersection(markers1[marker_group])) for i in markers2.keys()] + tmp = [ + len(markers2[i].intersection(markers1[marker_group])) + for i in markers2.keys() + ] overlaps[j, :] = tmp return overlaps @@ -56,7 +59,8 @@ def _calc_jaccard(markers1: dict, markers2: dict): for j, marker_group in enumerate(markers1): tmp = [ - len(markers2[i].intersection(markers1[marker_group])) / len(markers2[i].union(markers1[marker_group])) + len(markers2[i].intersection(markers1[marker_group])) + / len(markers2[i].union(markers1[marker_group])) for i in markers2.keys() ] jacc_results[j, :] = tmp @@ -154,11 +158,15 @@ def marker_gene_overlap( # Test user inputs if inplace: raise NotImplementedError( - 'Writing Pandas dataframes to h5ad is currently under development.' '\nPlease use `inplace=False`.' + 'Writing Pandas dataframes to h5ad is currently under development.' + '\nPlease use `inplace=False`.' ) if key not in adata.uns: - raise ValueError('Could not find marker gene data. ' 'Please run `sc.tl.rank_genes_groups()` first.') + raise ValueError( + 'Could not find marker gene data. ' + 'Please run `sc.tl.rank_genes_groups()` first.' + ) avail_methods = {'overlap_count', 'overlap_coef', 'jaccard', 'enrich'} if method not in avail_methods: @@ -176,9 +184,14 @@ def marker_gene_overlap( if not all(isinstance(val, cabc.Set) for val in reference_markers.values()): try: - reference_markers = {key: set(val) for key, val in reference_markers.items()} + reference_markers = { + key: set(val) for key, val in reference_markers.items() + } except Exception: - raise ValueError('Please ensure that `reference_markers` contains ' 'sets or lists of markers as values.') + raise ValueError( + 'Please ensure that `reference_markers` contains ' + 'sets or lists of markers as values.' + ) if adj_pval_threshold is not None: if 'pvals_adj' not in adata.uns[key]: @@ -189,19 +202,26 @@ def marker_gene_overlap( ) if adj_pval_threshold < 0: - logg.warning('`adj_pval_threshold` was set below 0. Threshold will be set to 0.') + logg.warning( + '`adj_pval_threshold` was set below 0. Threshold will be set to 0.' + ) adj_pval_threshold = 0 elif adj_pval_threshold > 1: - logg.warning('`adj_pval_threshold` was set above 1. Threshold will be set to 1.') + logg.warning( + '`adj_pval_threshold` was set above 1. Threshold will be set to 1.' + ) adj_pval_threshold = 1 if top_n_markers is not None: logg.warning( - 'Both `adj_pval_threshold` and `top_n_markers` is set. ' '`adj_pval_threshold` will be ignored.' + 'Both `adj_pval_threshold` and `top_n_markers` is set. ' + '`adj_pval_threshold` will be ignored.' ) if top_n_markers is not None and top_n_markers < 1: - logg.warning('`top_n_markers` was set below 1. `top_n_markers` will be set to 1.') + logg.warning( + '`top_n_markers` was set below 1. `top_n_markers` will be set to 1.' + ) top_n_markers = 1 # Get data-derived marker genes in a dictionary of sets @@ -229,12 +249,16 @@ def marker_gene_overlap( marker_match = _calc_overlap_count(reference_markers, data_markers) if normalize == 'reference': # Ensure rows sum to 1 - ref_lengths = np.array([len(reference_markers[m_group]) for m_group in reference_markers]) + ref_lengths = np.array( + [len(reference_markers[m_group]) for m_group in reference_markers] + ) marker_match = marker_match / ref_lengths[:, np.newaxis] marker_match = np.nan_to_num(marker_match) elif normalize == 'data': # Ensure columns sum to 1 - data_lengths = np.array([len(data_markers[dat_group]) for dat_group in data_markers]) + data_lengths = np.array( + [len(data_markers[dat_group]) for dat_group in data_markers] + ) marker_match = marker_match / data_lengths marker_match = np.nan_to_num(marker_match) elif method == 'overlap_coef': @@ -252,7 +276,9 @@ def marker_gene_overlap( # Create a pandas dataframe with the results marker_groups = list(reference_markers.keys()) clusters = list(cluster_ids) - marker_matching_df = pd.DataFrame(marker_match, index=marker_groups, columns=clusters) + marker_matching_df = pd.DataFrame( + marker_match, index=marker_groups, columns=clusters + ) # Store the results if inplace: diff --git a/scanpy/tools/_paga.py b/scanpy/tools/_paga.py index 797fd12d5f..510f7ce202 100644 --- a/scanpy/tools/_paga.py +++ b/scanpy/tools/_paga.py @@ -98,7 +98,9 @@ def paga( """ check_neighbors = 'neighbors' if neighbors_key is None else neighbors_key if check_neighbors not in adata.uns: - raise ValueError('You need to run `pp.neighbors` first to compute a neighborhood graph.') + raise ValueError( + 'You need to run `pp.neighbors` first to compute a neighborhood graph.' + ) if groups is None: for k in ("leiden", "louvain"): if k in adata.obs.columns: @@ -159,7 +161,9 @@ def compute_connectivities(self): elif self._model == 'v1.0': return self._compute_connectivities_v1_0() else: - raise ValueError(f'`model` {self._model} needs to be one of {_AVAIL_MODELS}.') + raise ValueError( + f'`model` {self._model} needs to be one of {_AVAIL_MODELS}.' + ) def _compute_connectivities_v1_2(self): import igraph @@ -168,7 +172,9 @@ def _compute_connectivities_v1_2(self): ones.data = np.ones(len(ones.data)) # should be directed if we deal with distances g = _utils.get_igraph_from_adjacency(ones, directed=True) - vc = igraph.VertexClustering(g, membership=self._adata.obs[self._groups_key].cat.codes.values) + vc = igraph.VertexClustering( + g, membership=self._adata.obs[self._groups_key].cat.codes.values + ) ns = vc.sizes() n = sum(ns) es_inner_cluster = [vc.subgraph(i).ecount() for i in range(len(ns))] @@ -202,7 +208,9 @@ def _compute_connectivities_v1_0(self): ones = self._neighbors.connectivities.copy() ones.data = np.ones(len(ones.data)) g = _utils.get_igraph_from_adjacency(ones) - vc = igraph.VertexClustering(g, membership=self._adata.obs[self._groups_key].cat.codes.values) + vc = igraph.VertexClustering( + g, membership=self._adata.obs[self._groups_key].cat.codes.values + ) ns = vc.sizes() cg = vc.cluster_graph(combine_edges='sum') inter_es = _utils.get_sparse_from_igraph(cg, weight_attr='weight') / 2 @@ -227,8 +235,13 @@ def _get_connectivities_tree_v1_2(self): inverse_connectivities = self.connectivities.copy() inverse_connectivities.data = 1.0 / inverse_connectivities.data connectivities_tree = minimum_spanning_tree(inverse_connectivities) - connectivities_tree_indices = [connectivities_tree[i].nonzero()[1] for i in range(connectivities_tree.shape[0])] - connectivities_tree = sp.sparse.lil_matrix(self.connectivities.shape, dtype=float) + connectivities_tree_indices = [ + connectivities_tree[i].nonzero()[1] + for i in range(connectivities_tree.shape[0]) + ] + connectivities_tree = sp.sparse.lil_matrix( + self.connectivities.shape, dtype=float + ) for i, neighbors in enumerate(connectivities_tree_indices): if len(neighbors) > 0: connectivities_tree[i, neighbors] = self.connectivities[i, neighbors] @@ -238,7 +251,10 @@ def _get_connectivities_tree_v1_0(self, inter_es): inverse_inter_es = inter_es.copy() inverse_inter_es.data = 1.0 / inverse_inter_es.data connectivities_tree = minimum_spanning_tree(inverse_inter_es) - connectivities_tree_indices = [connectivities_tree[i].nonzero()[1] for i in range(connectivities_tree.shape[0])] + connectivities_tree_indices = [ + connectivities_tree[i].nonzero()[1] + for i in range(connectivities_tree.shape[0]) + ] connectivities_tree = sp.sparse.lil_matrix(inter_es.shape, dtype=float) for i, neighbors in enumerate(connectivities_tree_indices): if len(neighbors) > 0: @@ -250,7 +266,9 @@ def compute_transitions(self): if vkey not in self._adata.uns: if 'velocyto_transitions' in self._adata.uns: self._adata.uns[vkey] = self._adata.uns['velocyto_transitions'] - logg.debug("The key 'velocyto_transitions' has been changed to 'velocity_graph'.") + logg.debug( + "The key 'velocyto_transitions' has been changed to 'velocity_graph'." + ) else: raise ValueError( 'The passed AnnData needs to have an `uns` annotation ' @@ -271,7 +289,9 @@ def compute_transitions(self): self._adata.uns[vkey].astype('bool'), directed=True, ) - vc = igraph.VertexClustering(g, membership=self._adata.obs[self._groups_key].cat.codes.values) + vc = igraph.VertexClustering( + g, membership=self._adata.obs[self._groups_key].cat.codes.values + ) # set combine_edges to False if you want self loops cg_full = vc.cluster_graph(combine_edges='sum') transitions = _utils.get_sparse_from_igraph(cg_full, weight_attr='weight') @@ -302,7 +322,9 @@ def compute_transitions_old(self): self._adata.uns['velocyto_transitions'], directed=True, ) - vc = igraph.VertexClustering(g, membership=self._adata.obs[self._groups_key].cat.codes.values) + vc = igraph.VertexClustering( + g, membership=self._adata.obs[self._groups_key].cat.codes.values + ) # this stores all single-cell edges in the cluster graph cg_full = vc.cluster_graph(combine_edges=False) # this is the boolean version that simply counts edges in the clustered graph @@ -310,7 +332,9 @@ def compute_transitions_old(self): self._adata.uns['velocyto_transitions'].astype('bool'), directed=True, ) - vc_bool = igraph.VertexClustering(g_bool, membership=self._adata.obs[self._groups_key].cat.codes.values) + vc_bool = igraph.VertexClustering( + g_bool, membership=self._adata.obs[self._groups_key].cat.codes.values + ) cg_bool = vc_bool.cluster_graph(combine_edges='sum') # collapsed version transitions = _utils.get_sparse_from_igraph(cg_bool, weight_attr='weight') total_n = self._neighbors.n_neighbors * np.array(vc_bool.sizes()) @@ -391,12 +415,16 @@ def paga_expression_entropies(adata) -> List[float]: """ from scipy.stats import entropy - groups_order, groups_masks = _utils.select_groups(adata, key=adata.uns['paga']['groups']) + groups_order, groups_masks = _utils.select_groups( + adata, key=adata.uns['paga']['groups'] + ) entropies = [] for mask in groups_masks: X_mask = adata.X[mask].todense() x_median = np.nanmedian(X_mask, axis=1, overwrite_input=True) - x_probs = (x_median - np.nanmin(x_median)) / (np.nanmax(x_median) - np.nanmin(x_median)) + x_probs = (x_median - np.nanmin(x_median)) / ( + np.nanmax(x_median) - np.nanmin(x_median) + ) entropies.append(entropy(x_probs)) return entropies @@ -450,7 +478,11 @@ def paga_compare_paths( import networkx as nx g1 = nx.Graph(adata1.uns['paga'][adjacency_key]) - g2 = nx.Graph(adata2.uns['paga'][adjacency_key2 if adjacency_key2 is not None else adjacency_key]) + g2 = nx.Graph( + adata2.uns['paga'][ + adjacency_key2 if adjacency_key2 is not None else adjacency_key + ] + ) leaf_nodes1 = [str(x) for x in g1.nodes() if g1.degree(x) == 1] logg.debug(f'leaf nodes in graph 1: {leaf_nodes1}') paga_groups = adata1.uns['paga']['groups'] @@ -505,14 +537,22 @@ def paga_compare_paths( path_mapped = [asso_groups1[link] for link in path1] path_compare = path2 path_compare_id = 2 - path_compare_orig_names = [[orig_names2[int(s)] for s in link] for link in path_compare] - path_mapped_orig_names = [[orig_names2[int(s)] for s in link] for link in path_mapped] + path_compare_orig_names = [ + [orig_names2[int(s)] for s in link] for link in path_compare + ] + path_mapped_orig_names = [ + [orig_names2[int(s)] for s in link] for link in path_mapped + ] else: path_mapped = [asso_groups2[link] for link in path2] path_compare = path1 path_compare_id = 1 - path_compare_orig_names = [[orig_names1[int(s)] for s in link] for link in path_compare] - path_mapped_orig_names = [[orig_names1[int(s)] for s in link] for link in path_mapped] + path_compare_orig_names = [ + [orig_names1[int(s)] for s in link] for link in path_compare + ] + path_mapped_orig_names = [ + [orig_names1[int(s)] for s in link] for link in path_mapped + ] n_agreeing_steps_path = 0 ip_progress = 0 for il, l in enumerate(path_compare[:-1]): @@ -520,7 +560,10 @@ def paga_compare_paths( if ( ip < ip_progress or l not in p - or not (ip + 1 < len(path_mapped) and path_compare[il + 1] in path_mapped[ip + 1]) + or not ( + ip + 1 < len(path_mapped) + and path_compare[il + 1] in path_mapped[ip + 1] + ) ): continue # make sure that a step backward leads us to the same value of l @@ -538,7 +581,8 @@ def paga_compare_paths( # was ok in the previous step poss = list(range(ip - 1, ip_progress - 2, -1)) logg.debug( - f' step(s) backward to position(s) {poss} ' 'in path_mapped are fine, too: valid step' + f' step(s) backward to position(s) {poss} ' + 'in path_mapped are fine, too: valid step' ) n_agreeing_steps_path += 1 ip_progress = ip + 1 diff --git a/scanpy/tools/_rank_genes_groups.py b/scanpy/tools/_rank_genes_groups.py index e096f5fd34..99393f691c 100644 --- a/scanpy/tools/_rank_genes_groups.py +++ b/scanpy/tools/_rank_genes_groups.py @@ -104,7 +104,9 @@ def __init__( else: self.expm1_func = np.expm1 - self.groups_order, self.groups_masks = _utils.select_groups(adata, groups, groupby) + self.groups_order, self.groups_masks = _utils.select_groups( + adata, groups, groupby + ) # Singlet groups cause division by zero errors invalid_groups_selected = set(self.groups_order) & set( @@ -169,7 +171,9 @@ def _basic_stats(self): else: mask_rest = self.groups_masks[self.ireference] X_rest = self.X[mask_rest] - self.means[self.ireference], self.vars[self.ireference] = _get_mean_var(X_rest) + self.means[self.ireference], self.vars[self.ireference] = _get_mean_var( + X_rest + ) # deleting the next line causes a memory leak for some reason del X_rest @@ -280,7 +284,10 @@ def wilcoxon(self, tie_correct): m_active = np.count_nonzero(mask_rest) if n_active <= 25 or m_active <= 25: - logg.hint('Few observations in a group for ' 'normal approximation (<=25). Lower test accuracy.') + logg.hint( + 'Few observations in a group for ' + 'normal approximation (<=25). Lower test accuracy.' + ) # Calculate rank sums for each chunk for the current mask for ranks, left, right in _ranks(self.X, mask, mask_rest): @@ -288,9 +295,13 @@ def wilcoxon(self, tie_correct): if tie_correct: T[left:right] = _tiecorrect(ranks) - std_dev = np.sqrt(T * n_active * m_active * (n_active + m_active + 1) / 12.0) + std_dev = np.sqrt( + T * n_active * m_active * (n_active + m_active + 1) / 12.0 + ) - scores = (scores - (n_active * ((n_active + m_active + 1) / 2.0))) / std_dev + scores = ( + scores - (n_active * ((n_active + m_active + 1) / 2.0)) + ) / std_dev scores[np.isnan(scores)] = 0 pvals = 2 * stats.distributions.norm.sf(np.abs(scores)) @@ -320,9 +331,13 @@ def wilcoxon(self, tie_correct): else: T_i = 1 - std_dev = np.sqrt(T_i * n_active * (n_cells - n_active) * (n_cells + 1) / 12.0) + std_dev = np.sqrt( + T_i * n_active * (n_cells - n_active) * (n_cells + 1) / 12.0 + ) - scores[group_index, :] = (scores[group_index, :] - (n_active * (n_cells + 1) / 2.0)) / std_dev + scores[group_index, :] = ( + scores[group_index, :] - (n_active * (n_cells + 1) / 2.0) + ) / std_dev scores[np.isnan(scores)] = 0 pvals = 2 * stats.distributions.norm.sf(np.abs(scores[group_index, :])) @@ -400,7 +415,9 @@ def compute_statistics( from statsmodels.stats.multitest import multipletests pvals[np.isnan(pvals)] = 1 - _, pvals_adj, _, _ = multipletests(pvals, alpha=0.05, method='fdr_bh') + _, pvals_adj, _, _ = multipletests( + pvals, alpha=0.05, method='fdr_bh' + ) elif corr_method == 'bonferroni': pvals_adj = np.minimum(pvals * n_genes, 1.0) self.stats[group_name, 'pvals_adj'] = pvals_adj[global_indices] @@ -414,7 +431,9 @@ def compute_statistics( foldchanges = (self.expm1_func(mean_group) + 1e-9) / ( self.expm1_func(mean_rest) + 1e-9 ) # add small value to remove 0's - self.stats[group_name, 'logfoldchanges'] = np.log2(foldchanges[global_indices]) + self.stats[group_name, 'logfoldchanges'] = np.log2( + foldchanges[global_indices] + ) if n_genes_user is None: self.stats.index = self.var_names @@ -529,7 +548,9 @@ def rank_genes_groups( >>> sc.pl.rank_genes_groups(adata) """ if method is None: - logg.warning("Default of the method has been changed to 't-test' from 't-test_overestim_var'") + logg.warning( + "Default of the method has been changed to 't-test' from 't-test_overestim_var'" + ) method = 't-test' if 'only_positive' in kwds: @@ -559,7 +580,9 @@ def rank_genes_groups( groups_order += [reference] if reference != 'rest' and reference not in adata.obs[groupby].cat.categories: cats = adata.obs[groupby].cat.categories.tolist() - raise ValueError(f'reference = {reference} needs to be one of groupby = {cats}.') + raise ValueError( + f'reference = {reference} needs to be one of groupby = {cats}.' + ) if key_added is None: key_added = 'rank_genes_groups' @@ -585,11 +608,15 @@ def rank_genes_groups( logg.debug(f'consider {groupby!r} groups:') logg.debug(f'with sizes: {np.count_nonzero(test_obj.groups_masks, axis=1)}') - test_obj.compute_statistics(method, corr_method, n_genes_user, rankby_abs, tie_correct, **kwds) + test_obj.compute_statistics( + method, corr_method, n_genes_user, rankby_abs, tie_correct, **kwds + ) if test_obj.pts is not None: groups_names = [str(name) for name in test_obj.groups_order] - adata.uns[key_added]['pts'] = pd.DataFrame(test_obj.pts.T, index=test_obj.var_names, columns=groups_names) + adata.uns[key_added]['pts'] = pd.DataFrame( + test_obj.pts.T, index=test_obj.var_names, columns=groups_names + ) if test_obj.pts_rest is not None: adata.uns[key_added]['pts_rest'] = pd.DataFrame( test_obj.pts_rest.T, index=test_obj.var_names, columns=groups_names @@ -606,7 +633,9 @@ def rank_genes_groups( } for col in test_obj.stats.columns.levels[0]: - adata.uns[key_added][col] = test_obj.stats[col].to_records(index=False, column_dtypes=dtypes[col]) + adata.uns[key_added][col] = test_obj.stats[col].to_records( + index=False, column_dtypes=dtypes[col] + ) logg.info( ' finished', @@ -751,8 +780,12 @@ def expm1_func(x): X_out = sub_X[~in_group] if use_fraction: - fraction_in_cluster_matrix.loc[:, cluster] = adata.uns[key]['pts'][cluster].loc[var_names].values - fraction_out_cluster_matrix.loc[:, cluster] = adata.uns[key]['pts_rest'][cluster].loc[var_names].values + fraction_in_cluster_matrix.loc[:, cluster] = ( + adata.uns[key]['pts'][cluster].loc[var_names].values + ) + fraction_out_cluster_matrix.loc[:, cluster] = ( + adata.uns[key]['pts_rest'][cluster].loc[var_names].values + ) else: fraction_in_cluster_matrix.loc[:, cluster] = _calc_frac(X_in) fraction_out_cluster_matrix.loc[:, cluster] = _calc_frac(X_out) @@ -763,7 +796,8 @@ def expm1_func(x): mean_out_cluster = np.ravel(X_out.mean(0)) # compute fold change fold_change_matrix.loc[:, cluster] = np.log2( - (expm1_func(mean_in_cluster) + 1e-9) / (expm1_func(mean_out_cluster) + 1e-9) + (expm1_func(mean_in_cluster) + 1e-9) + / (expm1_func(mean_out_cluster) + 1e-9) ) # filter original_matrix diff --git a/scanpy/tools/_score_genes.py b/scanpy/tools/_score_genes.py index a02dfafa12..d336544f0d 100644 --- a/scanpy/tools/_score_genes.py +++ b/scanpy/tools/_score_genes.py @@ -32,7 +32,9 @@ def _sparse_nanmean(X, axis): # the average s = Y.sum(axis) - m = s / n_elements.astype('float32') # if we dont cast the int32 to float32, this will result in float64... + m = s / n_elements.astype( + 'float32' + ) # if we dont cast the int32 to float32, this will result in float64... return m @@ -125,16 +127,22 @@ def score_genes( use_raw = _check_use_raw(adata, use_raw) _adata = adata.raw if use_raw else adata - _adata_subset = _adata[:, gene_pool] if len(gene_pool) < len(_adata.var_names) else _adata + _adata_subset = ( + _adata[:, gene_pool] if len(gene_pool) < len(_adata.var_names) else _adata + ) if issparse(_adata_subset.X): obs_avg = pd.Series( np.array(_sparse_nanmean(_adata_subset.X, axis=0)).flatten(), index=gene_pool, ) # average expression of genes else: - obs_avg = pd.Series(np.nanmean(_adata_subset.X, axis=0), index=gene_pool) # average expression of genes + obs_avg = pd.Series( + np.nanmean(_adata_subset.X, axis=0), index=gene_pool + ) # average expression of genes - obs_avg = obs_avg[np.isfinite(obs_avg)] # Sometimes (and I don't know how) missing data may be there, with nansfor + obs_avg = obs_avg[ + np.isfinite(obs_avg) + ] # Sometimes (and I don't know how) missing data may be there, with nansfor n_items = int(np.round(len(obs_avg) / (n_bins - 1))) obs_cut = obs_avg.rank(method='min') // n_items @@ -231,7 +239,9 @@ def score_genes_cell_cycle( adata = adata.copy() if copy else adata ctrl_size = min(len(s_genes), len(g2m_genes)) # add s-score - score_genes(adata, gene_list=s_genes, score_name='S_score', ctrl_size=ctrl_size, **kwargs) + score_genes( + adata, gene_list=s_genes, score_name='S_score', ctrl_size=ctrl_size, **kwargs + ) # add g2m-score score_genes( adata, diff --git a/scanpy/tools/_sim.py b/scanpy/tools/_sim.py index e2e4fcbb68..5bf79f8189 100644 --- a/scanpy/tools/_sim.py +++ b/scanpy/tools/_sim.py @@ -164,7 +164,9 @@ def sample_dynamic_data(**params): for restart in range(nrRealizations + maxRestarts): # slightly break symmetry in initial conditions if 'toggleswitch' in model_key: - X0 = np.array([0.8 for i in range(grnsim.dim)]) + 0.01 * np.random.randn(grnsim.dim) + X0 = np.array( + [0.8 for i in range(grnsim.dim)] + ) + 0.01 * np.random.randn(grnsim.dim) X = grnsim.sim_model(tmax=tmax, X0=X0, noiseDyn=noiseDyn) # check branching check = True @@ -259,7 +261,9 @@ def sample_dynamic_data(**params): for filename in writedir.glob('sim*.txt'): pass logg.info(f'reading simulation results {filename}') - adata = readwrite._read(filename, first_column_names=True, suppress_cache_warning=True) + adata = readwrite._read( + filename, first_column_names=True, suppress_cache_warning=True + ) adata.uns['tmax_write'] = tmax / step return adata @@ -307,12 +311,16 @@ def write_data( Adj[i, i] = 1 np.savetxt(dir + '/adj_' + id + '.txt', Adj, header=header, fmt='%d') if Coupl.size > 0: - np.savetxt(dir + '/coupl_' + id + '.txt', Coupl, header=header, fmt='%10.6f') + np.savetxt( + dir + '/coupl_' + id + '.txt', Coupl, header=header, fmt='%10.6f' + ) # write model file if varNames and Coupl.size > 0: with (dir / f'model_{id}.txt').open('w') as f: f.write('# For each "variable = ", there must be a right hand side: \n') - f.write('# either an empty string or a python-style logical expression \n') + f.write( + '# either an empty string or a python-style logical expression \n' + ) f.write('# involving variable names, "or", "and", "(", ")". \n') f.write('# The order of equations matters! \n') f.write('# \n') @@ -328,7 +336,11 @@ def write_data( for gp in range(dim): for g in range(dim): if np.abs(Coupl[gp, g]) > 1e-10: - f.write(f'{names[gp]:10} ' f'{names[g]:10} ' f'{Coupl[gp, g]:10.3} \n') + f.write( + f'{names[gp]:10} ' + f'{names[g]:10} ' + f'{Coupl[gp, g]:10.3} \n' + ) # write simulated data # the binary mode option in the following line is a fix for python 3 # variable names @@ -384,7 +396,9 @@ def __init__( either string for predefined model, or directory with a model file and a couple matrix files """ - self.dim = dim if Coupl is None else Coupl.shape[0] # number of nodes / dimension of system + self.dim = ( + dim if Coupl is None else Coupl.shape[0] + ) # number of nodes / dimension of system self.maxnpar = 1 # maximal number of parents self.p_indep = 0.4 # fraction of independent genes self.model = model @@ -461,16 +475,24 @@ def Xdiff_hill(self, Xt): iparent = self.varNames[self.pas[child][iv]] x = Xt[iparent] threshold = 0.1 / np.abs(self.Coupl[ichild, iparent]) - Xdiff_syn_tuple *= self.hill_a(x, threshold) if v else self.hill_i(x, threshold) + Xdiff_syn_tuple *= ( + self.hill_a(x, threshold) if v else self.hill_i(x, threshold) + ) if verbosity > 0: - Xdiff_syn_tuple_str += f'{"a" if v else "i"}' f'({self.pas[child][iv]}, {threshold:.2})' + Xdiff_syn_tuple_str += ( + f'{"a" if v else "i"}' + f'({self.pas[child][iv]}, {threshold:.2})' + ) Xdiff_syn += Xdiff_syn_tuple if verbosity > 0: Xdiff_syn_str += ('+' if ituple != 0 else '') + Xdiff_syn_tuple_str # multiply with degradation term Xdiff[ichild] = self.invTimeStep * (Xdiff_syn - Xt[ichild]) if verbosity > 0: - Xdiff_str = f'{child}_{child}-{child} = ' f'{self.invTimeStep}*({Xdiff_syn_str}-{child})' + Xdiff_str = ( + f'{child}_{child}-{child} = ' + f'{self.invTimeStep}*({Xdiff_syn_str}-{child})' + ) settings.m(0, Xdiff_str) return Xdiff @@ -540,7 +562,9 @@ def read_model(self): # read couplings via names self.Coupl = np.zeros((self.dim, self.dim)) boolContinue = True - for line in self.model.open(): # open(self.model.replace('/model','/couplList')): + for ( + line + ) in self.model.open(): # open(self.model.replace('/model','/couplList')): if line.startswith('# coupling list:'): boolContinue = False if boolContinue: @@ -572,7 +596,9 @@ def set_coupl(self, Coupl=None): for g in range(self.dim): if np.abs(self.Coupl[gp, g] > 1e-10): pas.append(names[g]) - self.boolRules[names[gp]] = ''.join(pas[:1] + [' or ' + pa for pa in pas[1:]]) + self.boolRules[names[gp]] = ''.join( + pas[:1] + [' or ' + pa for pa in pas[1:]] + ) self.Adj_signed = np.sign(Coupl) elif self.model in ['6', '7', '8', '9', '10']: self.Adj_signed = np.zeros((self.dim, self.dim)) @@ -591,10 +617,14 @@ def set_coupl(self, Coupl=None): # settings.m(0,leafnodes,availnodes) while len(availnodes) != 0: # parent - parent_idx = np.random.choice(np.arange(0, len(leafnodes)), size=1, replace=False) + parent_idx = np.random.choice( + np.arange(0, len(leafnodes)), size=1, replace=False + ) parent = leafnodes[parent_idx] # children - children_ids = np.random.choice(np.arange(0, len(availnodes)), size=2, replace=False) + children_ids = np.random.choice( + np.arange(0, len(availnodes)), size=2, replace=False + ) children = availnodes[children_ids] settings.m(0, parent, children) self.Adj_signed[children, parent] = np.ones(2) @@ -621,7 +651,9 @@ def set_coupl(self, Coupl=None): # and the variable itself, therefore its # self.maxnpar+2 in the following line nr = np.random.randint(1, self.maxnpar + 2) - j_par = np.random.choice(np.arange(0, self.dim), size=nr, replace=False) + j_par = np.random.choice( + np.arange(0, self.dim), size=nr, replace=False + ) self.Adj[i, j_par] = 1 else: self.Adj[i, i] = 1 @@ -706,7 +738,9 @@ def sim_model_backwards(self, tmax, X0): X = np.zeros((tmax, self.dim)) X[tmax - 1] = X0 for t in range(tmax - 2, -1, -1): - sol = sp.optimize.root(self.sim_model_back_help, X[t + 1], args=(X[t + 1]), method='hybr') + sol = sp.optimize.root( + self.sim_model_back_help, X[t + 1], args=(X[t + 1]), method='hybr' + ) X[t] = sol.x return X @@ -716,12 +750,17 @@ def branch_init_model1(self, tmax=100): if Xfix[0] > 0.97 or Xfix[0] < 0.03: settings.m( 0, - '... either no fixed point in [0,1]^2! \n' + ' or fixed point is too close to bounds', + '... either no fixed point in [0,1]^2! \n' + + ' or fixed point is too close to bounds', ) return None # - XbackUp = self.sim_model_backwards(tmax=tmax / 3, X0=Xfix + np.array([0.02, -0.02])) - XbackDo = self.sim_model_backwards(tmax=tmax / 3, X0=Xfix + np.array([-0.02, -0.02])) + XbackUp = self.sim_model_backwards( + tmax=tmax / 3, X0=Xfix + np.array([0.02, -0.02]) + ) + XbackDo = self.sim_model_backwards( + tmax=tmax / 3, X0=Xfix + np.array([-0.02, -0.02]) + ) # Xup = self.sim_model(tmax=tmax, X0=XbackUp[0]) Xdo = self.sim_model(tmax=tmax, X0=XbackDo[0]) @@ -744,7 +783,13 @@ def parents_from_boolRule(self, rule): Returns list of parents. """ - rule_pa = rule.replace('(', '').replace(')', '').replace('or', '').replace('and', '').replace('not', '') + rule_pa = ( + rule.replace('(', '') + .replace(')', '') + .replace('or', '') + .replace('and', '') + .replace('not', '') + ) rule_pa = rule_pa.split() # if there are no parents, continue if not rule_pa: @@ -791,13 +836,17 @@ def build_boolCoeff(self): raise ValueError(f'specify coupling value for {key} <- {g}') else: if np.abs(self.Coupl[self.varNames[key], g]) > 1e-10: - raise ValueError('there should be no coupling value for ' f'{key} <- {g}') + raise ValueError( + 'there should be no coupling value for ' f'{key} <- {g}' + ) if self.verbosity > 1: settings.m(0, '...' + key) settings.m(0, rule) settings.m(0, rule_pa) # noqa: F821 # now evaluate coefficients - for tuple in list(itertools.product([False, True], repeat=len(self.pas[key]))): + for tuple in list( + itertools.product([False, True], repeat=len(self.pas[key])) + ): if self.process_rule(rule, self.pas[key], tuple): self.boolCoeff[key].append(tuple) # @@ -925,7 +974,9 @@ def check_nocycles(Adj: np.ndarray, verbosity: int = 2) -> bool: return True -def sample_coupling_matrix(dim: int = 3, connectivity: float = 0.5) -> Tuple[np.ndarray, np.ndarray, np.ndarray, int]: +def sample_coupling_matrix( + dim: int = 3, connectivity: float = 0.5 +) -> Tuple[np.ndarray, np.ndarray, np.ndarray, int]: """\ Sample coupling matrix. @@ -975,7 +1026,9 @@ def sample_coupling_matrix(dim: int = 3, connectivity: float = 0.5) -> Tuple[np. check = True break if not check: - raise ValueError('did not find graph without cycles after' f'{max_trial} trials') + raise ValueError( + 'did not find graph without cycles after' f'{max_trial} trials' + ) return Coupl, Adj, Adj_signed, n_edges @@ -1167,7 +1220,8 @@ def sample_static_data(model, dir, verbosity=0): # command line options p = argparse.ArgumentParser( description=( - 'Simulate stochastic discrete-time dynamical systems,\n' 'in particular gene regulatory networks.' + 'Simulate stochastic discrete-time dynamical systems,\n' + 'in particular gene regulatory networks.' ), formatter_class=argparse.RawDescriptionHelpFormatter, epilog=( @@ -1206,7 +1260,10 @@ def sample_static_data(model, dir, verbosity=0): model = dir.name.split('_')[0] settings.m(0, f'...model is: {model!r}') if dir.is_dir() and 'test' not in str(dir): - message = f'directory {dir} already exists, ' 'remove it and continue? [y/n, press enter]' + message = ( + f'directory {dir} already exists, ' + 'remove it and continue? [y/n, press enter]' + ) if str(input(message)) != 'y': settings.m(0, ' ...quit program execution') sys.exit() diff --git a/scanpy/tools/_top_genes.py b/scanpy/tools/_top_genes.py index 855fef2222..28a9390590 100644 --- a/scanpy/tools/_top_genes.py +++ b/scanpy/tools/_top_genes.py @@ -180,7 +180,9 @@ def ROC_AUC_analysis( fpr[name_list[i]], tpr[name_list[i]], thresholds[name_list[i]], - ) = metrics.roc_curve(y_true, y_score, pos_label=None, sample_weight=None, drop_intermediate=False) + ) = metrics.roc_curve( + y_true, y_score, pos_label=None, sample_weight=None, drop_intermediate=False + ) roc_auc[name_list[i]] = metrics.auc(fpr[name_list[i]], tpr[name_list[i]]) adata.uns['ROCfpr' + groupby + str(group)] = fpr adata.uns['ROCtpr' + groupby + str(group)] = tpr diff --git a/scanpy/tools/_tsne_fix.py b/scanpy/tools/_tsne_fix.py index 8ee49cd98c..d5a7b663b5 100644 --- a/scanpy/tools/_tsne_fix.py +++ b/scanpy/tools/_tsne_fix.py @@ -125,12 +125,16 @@ def _gradient_descent( if verbose >= 2: print( "[t-SNE] Iteration %d: did not make any progress " - "during the last %d episodes. Finished." % (i + 1, n_iter_without_progress) + "during the last %d episodes. Finished." + % (i + 1, n_iter_without_progress) ) break if grad_norm <= min_grad_norm: if verbose >= 2: - print("[t-SNE] Iteration %d: gradient norm %f. Finished." % (i + 1, grad_norm)) + print( + "[t-SNE] Iteration %d: gradient norm %f. Finished." + % (i + 1, grad_norm) + ) break if error_diff <= min_error_diff: if verbose >= 2: diff --git a/scanpy/tools/_umap.py b/scanpy/tools/_umap.py index 4267fc479e..28ee17c229 100644 --- a/scanpy/tools/_umap.py +++ b/scanpy/tools/_umap.py @@ -122,13 +122,17 @@ def umap( neighbors_key = 'neighbors' if neighbors_key not in adata.uns: - raise ValueError(f'Did not find .uns["{neighbors_key}"]. Run `sc.pp.neighbors` first.') + raise ValueError( + f'Did not find .uns["{neighbors_key}"]. Run `sc.pp.neighbors` first.' + ) start = logg.info('computing UMAP') neighbors = NeighborsView(adata, neighbors_key) if 'params' not in neighbors or neighbors['params']['method'] != 'umap': - logg.warning(f'.obsp["{neighbors["connectivities_key"]}"] have not been computed using umap') + logg.warning( + f'.obsp["{neighbors["connectivities_key"]}"] have not been computed using umap' + ) # Compat for umap 0.4 -> 0.5 with warnings.catch_warnings(): @@ -163,7 +167,9 @@ def simplicial_set_embedding(*args, **kwargs): if isinstance(init_pos, str) and init_pos in adata.obsm.keys(): init_coords = adata.obsm[init_pos] elif isinstance(init_pos, str) and init_pos == 'paga': - init_coords = get_init_pos_from_paga(adata, random_state=random_state, neighbors_key=neighbors_key) + init_coords = get_init_pos_from_paga( + adata, random_state=random_state, neighbors_key=neighbors_key + ) else: init_coords = init_pos # Let umap handle it if hasattr(init_coords, "dtype"): @@ -210,7 +216,9 @@ def simplicial_set_embedding(*args, **kwargs): from cuml import UMAP n_neighbors = neighbors['params']['n_neighbors'] - n_epochs = 500 if maxiter is None else maxiter # 0 is not a valid value for rapids, unlike original umap + n_epochs = ( + 500 if maxiter is None else maxiter + ) # 0 is not a valid value for rapids, unlike original umap X_contiguous = np.ascontiguousarray(X, dtype=np.float32) umap = UMAP( n_neighbors=n_neighbors, diff --git a/scanpy/tools/_utils.py b/scanpy/tools/_utils.py index 3d257fe1d2..856a3f1b45 100644 --- a/scanpy/tools/_utils.py +++ b/scanpy/tools/_utils.py @@ -30,7 +30,9 @@ def _choose_representation(adata, use_rep=None, n_pcs=None, silent=False): if adata.n_vars > settings.N_PCS: if 'X_pca' in adata.obsm.keys(): if n_pcs is not None and n_pcs > adata.obsm['X_pca'].shape[1]: - raise ValueError('`X_pca` does not have enough PCs. Rerun `sc.pp.pca` with adjusted `n_comps`.') + raise ValueError( + '`X_pca` does not have enough PCs. Rerun `sc.pp.pca` with adjusted `n_comps`.' + ) X = adata.obsm['X_pca'][:, :n_pcs] logg.info(f' using \'X_pca\' with n_pcs = {X.shape[1]}') else: @@ -52,7 +54,10 @@ def _choose_representation(adata, use_rep=None, n_pcs=None, silent=False): elif use_rep == 'X': X = adata.X else: - raise ValueError('Did not find {} in `.obsm.keys()`. ' 'You need to compute it first.'.format(use_rep)) + raise ValueError( + 'Did not find {} in `.obsm.keys()`. ' + 'You need to compute it first.'.format(use_rep) + ) settings.verbosity = verbosity # resetting verbosity return X @@ -88,7 +93,9 @@ def preprocess_with_pca(adata, n_pcs: Optional[int] = None, random_state=0): return adata.X -def get_init_pos_from_paga(adata, adjacency=None, random_state=0, neighbors_key=None, obsp=None): +def get_init_pos_from_paga( + adata, adjacency=None, random_state=0, neighbors_key=None, obsp=None +): np.random.seed(random_state) if adjacency is None: adjacency = _choose_graph(adata, obsp, neighbors_key) @@ -110,5 +117,7 @@ def get_init_pos_from_paga(adata, adjacency=None, random_state=0, neighbors_key= else: init_pos[subset] = group_pos else: - raise ValueError('Plot PAGA first, so that adata.uns[\'paga\']' 'with key \'pos\'.') + raise ValueError( + 'Plot PAGA first, so that adata.uns[\'paga\']' 'with key \'pos\'.' + ) return init_pos diff --git a/scanpy/tools/_utils_clustering.py b/scanpy/tools/_utils_clustering.py index b46b9a2387..f7b331e6ca 100644 --- a/scanpy/tools/_utils_clustering.py +++ b/scanpy/tools/_utils_clustering.py @@ -1,4 +1,6 @@ -def rename_groups(adata, key_added, restrict_key, restrict_categories, restrict_indices, groups): +def rename_groups( + adata, key_added, restrict_key, restrict_categories, restrict_indices, groups +): key_added = restrict_key + '_R' if key_added is None else key_added all_groups = adata.obs[restrict_key].astype('U') prefix = '-'.join(restrict_categories) + ',' @@ -9,10 +11,14 @@ def rename_groups(adata, key_added, restrict_key, restrict_categories, restrict_ def restrict_adjacency(adata, restrict_key, restrict_categories, adjacency): if not isinstance(restrict_categories[0], str): - raise ValueError('You need to use strings to label categories, ' 'e.g. \'1\' instead of 1.') + raise ValueError( + 'You need to use strings to label categories, ' 'e.g. \'1\' instead of 1.' + ) for c in restrict_categories: if c not in adata.obs[restrict_key].cat.categories: - raise ValueError('\'{}\' is not a valid category for \'{}\''.format(c, restrict_key)) + raise ValueError( + '\'{}\' is not a valid category for \'{}\''.format(c, restrict_key) + ) restrict_indices = adata.obs[restrict_key].isin(restrict_categories).values adjacency = adjacency[restrict_indices, :] adjacency = adjacency[:, restrict_indices] diff --git a/setup.py b/setup.py index f0a8a941f6..e468a67378 100644 --- a/setup.py +++ b/setup.py @@ -25,7 +25,10 @@ author_email=metadata['author-email'], license='BSD', python_requires='>=3.6', - install_requires=[line.strip() for line in Path('requirements.txt').read_text('utf-8').splitlines()], + install_requires=[ + line.strip() + for line in Path('requirements.txt').read_text('utf-8').splitlines() + ], extras_require=dict( louvain=['python-igraph', 'louvain>=0.6,!=0.6.2'], leiden=['python-igraph', 'leidenalg'], From ae43e3dd4f4ad225333c91b35a658b150db608cd Mon Sep 17 00:00:00 2001 From: Zethson Date: Thu, 25 Feb 2021 10:45:59 +0100 Subject: [PATCH 07/85] fix comment character in .flake8 Signed-off-by: Zethson --- .flake8 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.flake8 b/.flake8 index 1a0e05ba84..580142445e 100644 --- a/.flake8 +++ b/.flake8 @@ -1,5 +1,5 @@ # Can't yet be moved to the pyproject.toml due to https://gitlab.com/pycqa/flake8/-/issues/428#note_251982786 [flake8] max-line-length = 88 -// switched off since they conflict with black's standards +# switched off since they conflict with black's standards ignore = F401, W503, E501, E203, E231, W504, E402, E126, E712, E741 From 7db4e604cda0999ff838f829e2c86dee3770f5c3 Mon Sep 17 00:00:00 2001 From: Zethson Date: Thu, 25 Feb 2021 11:03:35 +0100 Subject: [PATCH 08/85] fix test Signed-off-by: Zethson --- scanpy/tests/test_pca.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scanpy/tests/test_pca.py b/scanpy/tests/test_pca.py index 2dc827c2f6..975f7ee34d 100644 --- a/scanpy/tests/test_pca.py +++ b/scanpy/tests/test_pca.py @@ -3,6 +3,7 @@ from anndata import AnnData import scanpy as sc +from scanpy.tests.fixtures import array_type, float_dtype from anndata.tests.helpers import assert_equal A_list = [ @@ -37,7 +38,7 @@ ) -def test_pca_transform(array_type): +def test_pca_transform(array_type): # noqa: F811 A = array_type(A_list).astype('float32') A_pca_abs = np.abs(A_pca) A_svd_abs = np.abs(A_svd) @@ -100,7 +101,7 @@ def test_pca_sparse(pbmc3k_normalized): # This will take a while to run, but irreproducibility may # not show up for float32 unless the matrix is large enough -def test_pca_reproducible(pbmc3k_normalized, array_type, float_dtype): +def test_pca_reproducible(pbmc3k_normalized, array_type, float_dtype): # noqa: F811 pbmc = pbmc3k_normalized pbmc.X = array_type(pbmc.X) From 48f0648763741fef1652905acc057bd0bf8c04cd Mon Sep 17 00:00:00 2001 From: Zethson Date: Thu, 25 Feb 2021 12:44:50 +0100 Subject: [PATCH 09/85] black Signed-off-by: Zethson --- .pre-commit-config.yaml | 1 - pyproject.toml | 2 +- scanpy/neighbors/__init__.py | 1 + scanpy/preprocessing/_deprecated/highly_variable_genes.py | 6 ++---- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 97cc88d8ec..861b71dbfc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,4 +7,3 @@ repos: rev: 3.8.4 hooks: - id: flake8 - exclude: scanpy/tests/ diff --git a/pyproject.toml b/pyproject.toml index e1d37256b8..5e4e46f851 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,7 @@ omit = ['*/tests/*'] [tool.black] line-length = 88 -target-version = ['py38'] +target-version = ['py36'] skip-string-normalization = true exclude = ''' /build/.* diff --git a/scanpy/neighbors/__init__.py b/scanpy/neighbors/__init__.py index 3f56887cae..6faf932e20 100644 --- a/scanpy/neighbors/__init__.py +++ b/scanpy/neighbors/__init__.py @@ -124,6 +124,7 @@ def neighbors( **distances** : sparse matrix of dtype `float32`. Instead of decaying weights, this stores distances for each pair of neighbors. + Notes ----- If `method='umap'`, it's highly recommended to install pynndescent ``pip install pynndescent``. diff --git a/scanpy/preprocessing/_deprecated/highly_variable_genes.py b/scanpy/preprocessing/_deprecated/highly_variable_genes.py index 1487b60feb..570a5f9f25 100644 --- a/scanpy/preprocessing/_deprecated/highly_variable_genes.py +++ b/scanpy/preprocessing/_deprecated/highly_variable_genes.py @@ -170,10 +170,8 @@ def filter_genes_dispersion( disp_mean_bin[one_gene_per_bin] = 0 # actually do the normalization df['dispersion_norm'] = ( - df['dispersion'].values - - disp_mean_bin[ - df['mean_bin'].values - ].values # use values here as index differs + df['dispersion'].values # use values here as index differs + - disp_mean_bin[df['mean_bin'].values].values ) / disp_std_bin[df['mean_bin'].values].values elif flavor == 'cell_ranger': from statsmodels import robust From e742c668f69a0178034cf5cb184652e5ffd5f031 Mon Sep 17 00:00:00 2001 From: Zethson Date: Thu, 25 Feb 2021 12:57:48 +0100 Subject: [PATCH 10/85] review round 2 Signed-off-by: Zethson --- scanpy/plotting/_tools/__init__.py | 2 +- scanpy/plotting/_tools/scatterplots.py | 1 - scanpy/preprocessing/_highly_variable_genes.py | 6 ++---- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/scanpy/plotting/_tools/__init__.py b/scanpy/plotting/_tools/__init__.py index e12eadc5d6..7f7b76e04f 100644 --- a/scanpy/plotting/_tools/__init__.py +++ b/scanpy/plotting/_tools/__init__.py @@ -56,7 +56,7 @@ def pca_overview(adata: AnnData, **params): show = params['show'] if 'show' in params else None if 'show' in params: del params['show'] - scatterplots.pca(adata, **params, show=False) # noqa: F821 + pca(adata, **params, show=False) pca_loadings(adata, show=False) pca_variance_ratio(adata, show=show) diff --git a/scanpy/plotting/_tools/scatterplots.py b/scanpy/plotting/_tools/scatterplots.py index 9cf42fb762..1c17f4566d 100644 --- a/scanpy/plotting/_tools/scatterplots.py +++ b/scanpy/plotting/_tools/scatterplots.py @@ -220,7 +220,6 @@ def embedding( else: size = 120000 / adata.shape[0] - ### # make the plots axs = [] import itertools diff --git a/scanpy/preprocessing/_highly_variable_genes.py b/scanpy/preprocessing/_highly_variable_genes.py index 039a57cb58..7d0ae3f235 100644 --- a/scanpy/preprocessing/_highly_variable_genes.py +++ b/scanpy/preprocessing/_highly_variable_genes.py @@ -236,10 +236,8 @@ def _highly_variable_genes_single_batch( disp_mean_bin[one_gene_per_bin.values] = 0 # actually do the normalization df['dispersions_norm'] = ( - df['dispersions'].values - - disp_mean_bin[ - df['mean_bin'].values - ].values # use values here as index differs + df['dispersions'].values # use values here as index differs + - disp_mean_bin[df['mean_bin'].values].values ) / disp_std_bin[df['mean_bin'].values].values elif flavor == 'cell_ranger': from statsmodels import robust From a5b12900eeda627cbe06f007ab72a7e91267e59c Mon Sep 17 00:00:00 2001 From: Zethson Date: Thu, 25 Feb 2021 13:21:16 +0100 Subject: [PATCH 11/85] review round 3 Signed-off-by: Zethson --- .flake8 | 2 +- scanpy/plotting/_utils.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.flake8 b/.flake8 index 580142445e..1e9e5385cc 100644 --- a/.flake8 +++ b/.flake8 @@ -2,4 +2,4 @@ [flake8] max-line-length = 88 # switched off since they conflict with black's standards -ignore = F401, W503, E501, E203, E231, W504, E402, E126, E712, E741 +ignore = F401, W503, E501, E203, E231, W504, E402, E126, E712, E741, E266 diff --git a/scanpy/plotting/_utils.py b/scanpy/plotting/_utils.py index 070214a572..8239ff4dc9 100644 --- a/scanpy/plotting/_utils.py +++ b/scanpy/plotting/_utils.py @@ -937,7 +937,7 @@ def make_pos(pos, node=root, currentLevel=0, parent=None, vert_loc=0): if levels is None: levels = make_levels({}) else: - levels = {level: {TOTAL: levels[level], CURRENT: 0} for level in levels} + levels = {l: {TOTAL: levels[l], CURRENT: 0} for l in levels} vert_gap = height / (max([level for level in levels]) + 1) return make_pos({}) From 718a06c4e148963bfb02a8ed31f0500150d58020 Mon Sep 17 00:00:00 2001 From: Zethson Date: Thu, 25 Feb 2021 13:30:19 +0100 Subject: [PATCH 12/85] readded double comments Signed-off-by: Zethson --- scanpy/tools/_top_genes.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/scanpy/tools/_top_genes.py b/scanpy/tools/_top_genes.py index 28a9390590..97f0b7386c 100644 --- a/scanpy/tools/_top_genes.py +++ b/scanpy/tools/_top_genes.py @@ -191,11 +191,11 @@ def ROC_AUC_analysis( def subsampled_estimates(mask, mask_rest=None, precision=0.01, probability=0.99): - # Simple method that can be called by rank_gene_group. It uses masks that have been passed to the function and - # calculates how much has to be subsampled in order to reach a certain precision with a certain probability - # Then it subsamples for mask, mask rest - # Since convergence speed varies, we take the slower one, i.e. the variance. This might have future speed-up - # potential + ## Simple method that can be called by rank_gene_group. It uses masks that have been passed to the function and + ## calculates how much has to be subsampled in order to reach a certain precision with a certain probability + ## Then it subsamples for mask, mask rest + ## Since convergence speed varies, we take the slower one, i.e. the variance. This might have future speed-up + ## potential if mask_rest is None: mask_rest = ~mask # TODO: DO precision calculation for mean variance shared @@ -204,16 +204,16 @@ def subsampled_estimates(mask, mask_rest=None, precision=0.01, probability=0.99) def dominated_ROC_elimination(adata, grouby): - # This tool has the purpose to take a set of genes (possibly already pre-selected) and analyze AUC. - # Those and only those are eliminated who are dominated completely - # TODO: Potentially (But not till tomorrow), this can be adapted to only consider the AUC in the given - # TODO: optimization frame + ## This tool has the purpose to take a set of genes (possibly already pre-selected) and analyze AUC. + ## Those and only those are eliminated who are dominated completely + ## TODO: Potentially (But not till tomorrow), this can be adapted to only consider the AUC in the given + ## TODO: optimization frame pass def _gene_preselection(adata, mask, thresholds): - # This tool serves to - # It is not thought to be addressed directly but rather using rank_genes_group or ROC analysis or comparable - # TODO: Pass back a truncated adata object with only those genes that fullfill thresholding criterias - # This function should be accessible by both rank_genes_groups and ROC_curve analysis + ## This tool serves to + ## It is not thought to be addressed directly but rather using rank_genes_group or ROC analysis or comparable + ## TODO: Pass back a truncated adata object with only those genes that fullfill thresholding criterias + ## This function should be accessible by both rank_genes_groups and ROC_curve analysis pass From 2a0a19de2b5bf7a3615249686ce2f3d69127e023 Mon Sep 17 00:00:00 2001 From: Zethson Date: Thu, 25 Feb 2021 14:17:43 +0100 Subject: [PATCH 13/85] Ignoring E262 & reverted comment Signed-off-by: Zethson --- .flake8 | 2 +- scanpy/tools/_dpt.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.flake8 b/.flake8 index 1e9e5385cc..636b14206b 100644 --- a/.flake8 +++ b/.flake8 @@ -2,4 +2,4 @@ [flake8] max-line-length = 88 # switched off since they conflict with black's standards -ignore = F401, W503, E501, E203, E231, W504, E402, E126, E712, E741, E266 +ignore = F401, W503, E501, E203, E231, W504, E402, E126, E712, E741, E266, E262 diff --git a/scanpy/tools/_dpt.py b/scanpy/tools/_dpt.py index 35937dc7f0..6521afecf3 100644 --- a/scanpy/tools/_dpt.py +++ b/scanpy/tools/_dpt.py @@ -899,9 +899,9 @@ def _detect_branching_single_haghverdi16(self, Dseg, tips): # permutations of tip cells ps = [ [0, 1, 2], # start by computing distances from the first tip - [1, 2, 0], # -"- second tip + [1, 2, 0], # -"- second tip [2, 0, 1], - ] # -"- third tip + ] # -"- third tip for i, p in enumerate(ps): ssegs.append(self.__detect_branching_haghverdi16(Dseg, tips[p])) return ssegs From ebb2b01c02b78120273513b283dd820d4fca1a08 Mon Sep 17 00:00:00 2001 From: Zethson Date: Thu, 25 Feb 2021 14:22:16 +0100 Subject: [PATCH 14/85] using self for obs_tidy Signed-off-by: Zethson --- scanpy/plotting/_baseplot_class.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scanpy/plotting/_baseplot_class.py b/scanpy/plotting/_baseplot_class.py index 6c7e417402..f8edc819dd 100644 --- a/scanpy/plotting/_baseplot_class.py +++ b/scanpy/plotting/_baseplot_class.py @@ -121,13 +121,13 @@ def __init__( if categories_order is not None: if set(self.obs_tidy.index.categories) != set(categories_order): logg.error( - "Please check that the categories given by " # noqa: F821 + "Please check that the categories given by " "the `order` parameter match the categories that " "want to be reordered.\n\n" "Mismatch: " - f"{set(obs_tidy.index.categories).difference(categories_order)}\n\n" # noqa: F821 + f"{set(self.obs_tidy.index.categories).difference(categories_order)}\n\n" f"Given order categories: {categories_order}\n\n" - f"{groupby} categories: {list(obs_tidy.index.categories)}\n" # noqa: F821 + f"{groupby} categories: {list(self.obs_tidy.index.categories)}\n" ) return From d2bb2a9b5ed44f8868951544f250064d7d5c6f20 Mon Sep 17 00:00:00 2001 From: Philipp A Date: Mon, 1 Mar 2021 11:27:19 +0100 Subject: [PATCH 15/85] Restore setup.py --- setup.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/setup.py b/setup.py index e468a67378..b75b02f38a 100644 --- a/setup.py +++ b/setup.py @@ -26,8 +26,7 @@ license='BSD', python_requires='>=3.6', install_requires=[ - line.strip() - for line in Path('requirements.txt').read_text('utf-8').splitlines() + l.strip() for l in Path('requirements.txt').read_text('utf-8').splitlines() ], extras_require=dict( louvain=['python-igraph', 'louvain>=0.6,!=0.6.2'], From ecc47a27b53b0d03bd29afc48eb6613b1b252b52 Mon Sep 17 00:00:00 2001 From: Sergei Rybakov Date: Wed, 24 Feb 2021 15:14:23 +0100 Subject: [PATCH 16/85] rm call of black test (#1690) --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 2c19e2dd85..1c6219b607 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,6 @@ matrix: - pip install .[dev,doc] script: - black . --check --diff - - python -m scanpy.tests.blackdiff 10 - python setup.py check --restructuredtext --strict - rst2html.py --halt=2 README.rst >/dev/null after_success: skip From f338863b01828ae0c1e0174b208ad4354ed01b5a Mon Sep 17 00:00:00 2001 From: Isaac Virshup Date: Thu, 25 Feb 2021 17:57:13 +1100 Subject: [PATCH 17/85] Fix print_versions for python<3.8 (#1691) --- docs/release-notes/1.7.2.rst | 12 ++++++++++++ docs/release-notes/release-latest.rst | 1 + scanpy/logging.py | 12 +++++++++++- scanpy/tests/test_logging.py | 25 ++++++++++++++++++++++++- 4 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 docs/release-notes/1.7.2.rst diff --git a/docs/release-notes/1.7.2.rst b/docs/release-notes/1.7.2.rst new file mode 100644 index 0000000000..077dc20927 --- /dev/null +++ b/docs/release-notes/1.7.2.rst @@ -0,0 +1,12 @@ +1.7.2 :small:`the future` +~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. rubric:: Performance enhancements + +.. rubric:: Bug fixes + +- :func:`scanpy.logging.print_versions` now works when `python<3.8` :pr:`1691` :smaller:`I Virshup` + +.. rubric:: Deprecations + +.. rubric:: Documentation \ No newline at end of file diff --git a/docs/release-notes/release-latest.rst b/docs/release-notes/release-latest.rst index 58f923f6c4..b3a0060a7d 100644 --- a/docs/release-notes/release-latest.rst +++ b/docs/release-notes/release-latest.rst @@ -6,5 +6,6 @@ Version 1.8 Version 1.7 ----------- +.. include:: /release-notes/1.7.2.rst .. include:: /release-notes/1.7.1.rst .. include:: /release-notes/1.7.0.rst diff --git a/scanpy/logging.py b/scanpy/logging.py index 5226817846..c0f810b281 100644 --- a/scanpy/logging.py +++ b/scanpy/logging.py @@ -165,7 +165,17 @@ def print_versions(*, file=None): stdout = sys.stdout try: buf = sys.stdout = io.StringIO() - sinfo(dependencies=True) + sinfo( + dependencies=True, + excludes=[ + 'builtins', + 'stdlib_list', + 'importlib_metadata', + # Special module present if test coverage being calculated + # https://gitlab.com/joelostblom/sinfo/-/issues/10 + "$coverage", + ], + ) finally: sys.stdout = stdout output = buf.getvalue() diff --git a/scanpy/tests/test_logging.py b/scanpy/tests/test_logging.py index abcc97d104..a954d85421 100644 --- a/scanpy/tests/test_logging.py +++ b/scanpy/tests/test_logging.py @@ -1,10 +1,12 @@ -import sys +from contextlib import redirect_stdout from datetime import datetime from io import StringIO +import sys import pytest from scanpy import Verbosity, settings as s, logging as l +import scanpy as sc @pytest.fixture @@ -91,3 +93,24 @@ def now(tz): assert counter == 4 and capsys.readouterr().err == '4 (0:00:02)\n' l.info('5 {time_passed}', time=start) assert counter == 5 and capsys.readouterr().err == '5 0:00:03\n' + + +@pytest.mark.parametrize( + "func", + [ + sc.logging.print_header, + sc.logging.print_versions, + sc.logging.print_version_and_date, + ], +) +def test_call_outputs(func): + """ + Tests that these functions print to stdout and don't error. + + Checks that https://github.com/theislab/scanpy/issues/1437 is fixed. + """ + output_io = StringIO() + with redirect_stdout(output_io): + func() + output = output_io.getvalue() + assert output != "" From ce68cd12464faa583112f8e652534008904259ef Mon Sep 17 00:00:00 2001 From: Isaac Virshup Date: Thu, 25 Feb 2021 18:59:11 +1100 Subject: [PATCH 18/85] add codecov so we can have a badge to point to (#1693) --- .azure-pipelines.yml | 4 ++++ .codecov.yml | 14 ++++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 .codecov.yml diff --git a/.azure-pipelines.yml b/.azure-pipelines.yml index 6b3c6692b8..8bb3d3325b 100644 --- a/.azure-pipelines.yml +++ b/.azure-pipelines.yml @@ -79,6 +79,10 @@ jobs: testResultsFormat: NUnit testRunTitle: 'Publish test results for Python $(python.version)' + - script: bash <(curl -s https://codecov.io/bash) + displayName: 'Upload to codecov.io' + condition: eq(variables['RUN_COVERAGE'], 'yes') + - job: CheckBuild pool: vmImage: 'ubuntu-18.04' diff --git a/.codecov.yml b/.codecov.yml new file mode 100644 index 0000000000..8e49cc3831 --- /dev/null +++ b/.codecov.yml @@ -0,0 +1,14 @@ +# Based on pydata/xarray +codecov: + require_ci_to_pass: yes + +coverage: + status: + project: + default: + # Require 1% coverage, i.e., always succeed + target: 1 + patch: false + changes: false + +comment: off \ No newline at end of file From b5cc4b69994e47c33a9ba5db181734f89f816219 Mon Sep 17 00:00:00 2001 From: Isaac Virshup Date: Thu, 25 Feb 2021 19:00:34 +1100 Subject: [PATCH 19/85] Attempt server-side search (#1672) --- docs/conf.py | 1 + setup.py | 1 + 2 files changed, 2 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index 504894c420..eba759716c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -51,6 +51,7 @@ # 'ipython_directive', # 'ipython_console_highlighting', 'scanpydoc', + "sphinx_search.extension", *[p.stem for p in (HERE / 'extensions').glob('*.py')], ] diff --git a/setup.py b/setup.py index b75b02f38a..cf0e22bf02 100644 --- a/setup.py +++ b/setup.py @@ -43,6 +43,7 @@ doc=[ 'sphinx>=3.2', 'sphinx_rtd_theme>=0.3.1', + 'readthedocs-sphinx-search', 'sphinx_autodoc_typehints', 'scanpydoc>=0.5', 'typing_extensions; python_version < "3.8"', # for `Literal` From 8b0d8f01c64351de0a53e9256c7ceac9d1d68c5c Mon Sep 17 00:00:00 2001 From: Philipp A Date: Mon, 1 Mar 2021 10:18:39 +0100 Subject: [PATCH 20/85] Fix paga_path (#1047) Fix paga_path Co-authored-by: Isaac Virshup --- docs/release-notes/1.7.2.rst | 1 + scanpy/plotting/_tools/paga.py | 13 +++++++------ scanpy/tests/_images/master_paga_path.png | Bin 0 -> 5421 bytes scanpy/tests/test_plotting.py | 17 +++++++++++++++++ 4 files changed, 25 insertions(+), 6 deletions(-) create mode 100644 scanpy/tests/_images/master_paga_path.png diff --git a/docs/release-notes/1.7.2.rst b/docs/release-notes/1.7.2.rst index 077dc20927..029c2eb746 100644 --- a/docs/release-notes/1.7.2.rst +++ b/docs/release-notes/1.7.2.rst @@ -5,6 +5,7 @@ .. rubric:: Bug fixes +- Fix :func:`scanpy.pl.paga_path` `TypeError` with recent versions of anndata :pr:`1047` :smaller:`P Angerer` - :func:`scanpy.logging.print_versions` now works when `python<3.8` :pr:`1691` :smaller:`I Virshup` .. rubric:: Deprecations diff --git a/scanpy/plotting/_tools/paga.py b/scanpy/plotting/_tools/paga.py index 99d6f4db78..4f565b18ba 100644 --- a/scanpy/plotting/_tools/paga.py +++ b/scanpy/plotting/_tools/paga.py @@ -13,6 +13,7 @@ from matplotlib import patheffects from matplotlib.axes import Axes from matplotlib.colors import is_color_like, Colormap +from scipy.sparse import issparse from .. import _utils from .._utils import matrix, _IGraphLayout, _FontWeight, _FontSize @@ -1097,12 +1098,12 @@ def moving_average(a): ] ) idcs = idcs[idcs_group] - if key in adata.obs_keys(): - x += list(adata.obs[key].values[idcs]) - else: - x += list(adata_X[:, key].X[idcs]) + values = ( + adata.obs[key].values if key in adata.obs_keys() else adata_X[:, key].X + )[idcs] + x += (values.A if issparse(values) else values).tolist() if ikey == 0: - groups += [group for i in range(len(idcs))] + groups += [group] * len(idcs) x_tick_locs.append(len(x)) for anno in annotations: series = adata.obs[anno] @@ -1128,7 +1129,7 @@ def moving_average(a): else: label = group x_tick_labels.append(label) - X = np.array(X) + X = np.asarray(X).squeeze() if as_heatmap: img = ax.imshow(X, aspect='auto', interpolation='nearest', cmap=color_map) if show_yticks: diff --git a/scanpy/tests/_images/master_paga_path.png b/scanpy/tests/_images/master_paga_path.png new file mode 100644 index 0000000000000000000000000000000000000000..abc13665099244b1e025129d932be85c2568add2 GIT binary patch literal 5421 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 literal 0 HcmV?d00001 diff --git a/scanpy/tests/test_plotting.py b/scanpy/tests/test_plotting.py index 8eee2c576e..bd4791b18a 100644 --- a/scanpy/tests/test_plotting.py +++ b/scanpy/tests/test_plotting.py @@ -999,6 +999,23 @@ def test_paga(image_comparer): save_and_compare_images('master_paga_pie') +def test_paga_path(image_comparer): + save_and_compare_images = image_comparer(ROOT, FIGS, tol=15) + + pbmc = sc.datasets.pbmc68k_reduced() + sc.tl.paga(pbmc, groups='bulk_labels') + + pbmc.uns['iroot'] = 0 + sc.tl.dpt(pbmc) + sc.pl.paga_path( + pbmc, + nodes=['Dendritic'], + keys=['HES4', 'SRM', 'CSTB'], + show=False, + ) + save_and_compare_images('master_paga_path') + + def test_no_copy(): # https://github.com/theislab/scanpy/issues/1000 # Tests that plotting functions don't make a copy from a view unless they From 24d1b2e1ed26b12684b36879fc08311bf76246aa Mon Sep 17 00:00:00 2001 From: Philipp A Date: Thu, 3 Dec 2020 11:04:35 +0100 Subject: [PATCH 21/85] Switch to flit This reverts commit d6457902 --- .azure-pipelines.yml | 8 +- .gitignore | 1 + .travis.yml | 1 - MANIFEST.in | 4 - docs/conf.py | 1 - docs/installation.rst | 23 +++- docs/release-notes/1.8.0.rst | 5 + pyproject.toml | 108 +++++++++++++++++- requirements.txt | 24 ---- scanpy/__init__.py | 81 +++++++------ scanpy/_metadata.py | 35 +++++- scanpy/_utils.py | 3 + ...test_docs.py => test_package_structure.py} | 26 ++++- setup.py | 82 ------------- 14 files changed, 235 insertions(+), 167 deletions(-) delete mode 100644 MANIFEST.in delete mode 100644 requirements.txt rename scanpy/tests/{test_docs.py => test_package_structure.py} (67%) delete mode 100644 setup.py diff --git a/.azure-pipelines.yml b/.azure-pipelines.yml index 8bb3d3325b..02970df9e1 100644 --- a/.azure-pipelines.yml +++ b/.azure-pipelines.yml @@ -28,8 +28,8 @@ jobs: - task: Cache@2 inputs: - key: '"python $(python.version)" | "$(Agent.OS)" | requirements.txt' - restoreKeys: | + key: '"python $(python.version)" | "$(Agent.OS)" | pyproject.toml' + restoreKeys: | python | "$(Agent.OS)" python path: $(PIP_CACHE_DIR) @@ -43,7 +43,7 @@ jobs: - script: | python -m pip install --upgrade pip pip install pytest-cov wheel - pip install -e .[dev,doc,test,louvain,leiden,magic,scvi,harmony,scrublet,scanorama] + pip install .[dev,doc,test,louvain,leiden,magic,scvi,harmony,scrublet,scanorama] displayName: 'Install dependencies' - script: | @@ -102,6 +102,6 @@ jobs: displayName: 'Display installed versions' - script: | - python setup.py sdist bdist_wheel + flit build twine check dist/* displayName: 'Build & Twine check' diff --git a/.gitignore b/.gitignore index 079c5b0cea..baa3c9b7f1 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,7 @@ __pycache__/ /scanpy.egg-info/ /*-env/ /env-*/ +/setup.py # OS stuff .DS_Store diff --git a/.travis.yml b/.travis.yml index 1c6219b607..cb5c7a96eb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,6 @@ matrix: - pip install .[dev,doc] script: - black . --check --diff - - python setup.py check --restructuredtext --strict - rst2html.py --halt=2 README.rst >/dev/null after_success: skip - name: "anndata dev" diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 98d1521a1c..0000000000 --- a/MANIFEST.in +++ /dev/null @@ -1,4 +0,0 @@ -# Exclude everything not needed for importing, building, and tests -exclude .coveragerc .editorconfig .gitattributes .gitignore .readthedocs.yml .travis.yml -exclude CONTRIBUTING.md -prune docs diff --git a/docs/conf.py b/docs/conf.py index eba759716c..2e2b3c5157 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,6 +1,5 @@ import os import sys -import warnings from pathlib import Path from datetime import datetime diff --git a/docs/installation.rst b/docs/installation.rst index 37bf2e592b..b51f9012eb 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -7,7 +7,7 @@ If you do not have a working installation of Python 3.6 (or later), consider installing Miniconda_ (see `Installing Miniconda`_). Then run:: conda install seaborn scikit-learn statsmodels numba pytables - conda install -c conda-forge python-igraph leidenalg + conda install -c conda-forge python-igraph leidenalg Pull Scanpy from `PyPI `__ (consider using ``pip3`` to access Python 3):: @@ -35,7 +35,9 @@ To work with the latest version `on GitHub`_: clone the repository and `cd` into its root directory. To install using symbolic links (stay up to date with your cloned version after you update with `git pull`) call:: - pip install -e . + flit install -s # from an activated venv or conda env + # or + flit install -s --python path/to/venv/bin/python If you intend to do development work, there are some extra dependencies you'll want. These can be install with `scanpy` via:: @@ -44,6 +46,21 @@ These can be install with `scanpy` via:: .. _on GitHub: https://github.com/theislab/scanpy +If you want to let conda_ handle the installations of dependencies, do:: + + pip install beni + beni pyproject.toml > environment.yml + conda env create -f environment.yml + conda activate scanpy + flit install -s + +On Windows, you might have to use `flit install --pth-file` +if you are not able to give yourself the `create symbolic links`_ privilege. +Be aware that a `conda bug`_ might prevent `conda list` from working then. + +.. _create symbolic links: https://docs.microsoft.com/en-us/windows/security/threat-protection/security-policy-settings/create-symbolic-links +.. _conda bug: https://github.com/conda/conda/issues/9074 + Docker ~~~~~~ If you're using Docker_, you can use the minimal `fastgenomics/scanpy`_ image from the Docker Hub. @@ -78,6 +95,8 @@ Download those and install them using `pip install ./path/to/file.whl` .. _compiling igraph: https://stackoverflow.com/q/29589696/247482 .. _unofficial binaries: https://www.lfd.uci.edu/~gohlke/pythonlibs/ +.. _conda: + Installing Miniconda ~~~~~~~~~~~~~~~~~~~~ After downloading Miniconda_, in a unix shell (Linux, Mac), run diff --git a/docs/release-notes/1.8.0.rst b/docs/release-notes/1.8.0.rst index f85c0c4fd1..b98676558a 100644 --- a/docs/release-notes/1.8.0.rst +++ b/docs/release-notes/1.8.0.rst @@ -3,6 +3,11 @@ .. rubric:: Features +- Switched to flit_ for building and deploying the package, + a simple tool with an easy to understand command line interface and metadata. + +.. _flit: https://flit.readthedocs.io/en/latest/ + .. rubric:: External tools .. rubric:: Performance enhancements diff --git a/pyproject.toml b/pyproject.toml index 5e4e46f851..901f1dd7e1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,16 +1,114 @@ [build-system] -requires = ['setuptools', 'setuptools_scm', 'wheel', 'pytoml'] -build-backend = 'setuptools.build_meta' +build-backend = 'flit_core.buildapi' +requires = [ + 'flit_core >=2,<4', + 'setuptools_scm', + 'pytoml', + 'importlib_metadata>=0.7; python_version < "3.8"', +] -# uses the format of tool.flit.metadata because we’ll move to it anyway -[tool.scanpy] -author = """ +[tool.flit.metadata] +module = 'scanpy' +author = """\ Alex Wolf, Philipp Angerer, Fidel Ramirez, Isaac Virshup, \ Sergei Rybakov, Gokcen Eraslan, Tom White, Malte Luecken, \ Davide Cittaro, Tobias Callies, Marius Lange, Andrés R. Muñoz-Rojas\ """ # We don’t need all emails, the main authors are sufficient. author-email = 'f.alex.wolf@gmx.de, philipp.angerer@helmholtz-muenchen.de' +description-file = 'README.rst' +home-page = 'http://github.com/theislab/scanpy' +urls = { Documentation = 'https://scanpy.readthedocs.io/' } +classifiers = [ + 'License :: OSI Approved :: BSD License', + 'Development Status :: 5 - Production/Stable', + 'Environment :: Console', + 'Framework :: Jupyter', + 'Intended Audience :: Developers', + 'Intended Audience :: Science/Research', + 'Natural Language :: English', + 'Operating System :: MacOS :: MacOS X', + 'Operating System :: Microsoft :: Windows', + 'Operating System :: POSIX :: Linux', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Topic :: Scientific/Engineering :: Bio-Informatics', + 'Topic :: Scientific/Engineering :: Visualization', +] +requires-python = '>=3.6' +requires = [ + 'anndata>=0.7.4', + # numpy needs a version due to #1320 + 'numpy>=1.17.0', + # Matplotlib 3.1 causes an error in 3d scatter plots (https://github.com/matplotlib/matplotlib/issues/14298) + # But matplotlib 3.0 causes one in heatmaps + 'matplotlib>=3.1.2', + 'pandas>=0.21', + 'scipy>=1.4', + 'seaborn', + 'h5py>=2.10.0', + 'tables', + 'tqdm', + 'scikit-learn>=0.21.2', + 'statsmodels>=0.10.0rc2', + 'patsy', + 'networkx>=2.3', + 'natsort', + 'joblib', + 'numba>=0.41.0', + 'umap-learn>=0.3.10', + 'legacy-api-wrap', + 'packaging', + 'sinfo', + # for getting the stable version + 'importlib_metadata>=0.7; python_version < "3.8"', +] + +[tool.flit.metadata.requires-extra] +louvain = ['python-igraph', 'louvain>=0.6,!=0.6.2'] +leiden = ['python-igraph', 'leidenalg'] +bbknn = ['bbknn'] +scvi = ['scvi==0.6.7'] +rapids = ['cudf>=0.9', 'cuml>=0.9', 'cugraph>=0.9'] +magic = ['magic-impute>=2.0'] +skmisc = ['scikit-misc>=0.1.3'] +harmony = ['harmonypy'] +scanorama = ['scanorama'] +scrublet = ['scrublet'] +dev = [ + # getting the dev version + 'setuptools_scm', + 'pytoml', + # static checking + 'black>=20.8b1', + 'docutils', +] +doc = [ + 'sphinx>=3.2', + 'sphinx-rtd-theme>=0.3.1', + 'sphinx-autodoc-typehints', + 'readthedocs-sphinx-search', + 'scanpydoc>=0.5', + 'typing_extensions; python_version < "3.8"', # for `Literal` +] +test = [ + 'pytest>=4.4', + 'pytest-nunit', + 'dask[array]!=2.17.0', + 'fsspec', + 'zappy', + 'zarr', + 'profimp', + # Test the metadata while this exists: https://github.com/takluyver/flit/issues/387 + 'flit_core', +] + +[tool.flit.scripts] +scanpy = 'scanpy.cli:console_main' + +[tool.flit.sdist] +exclude = ['scanpy/tests'] [tool.pytest.ini_options] python_files = 'test_*.py' diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index e74f23c397..0000000000 --- a/requirements.txt +++ /dev/null @@ -1,24 +0,0 @@ -anndata>=0.7.4 -# numpy needs a version due to #1320 -numpy>=1.17.0 -# Matplotlib 3.1 causes an error in 3d scatter plots (https://github.com/matplotlib/matplotlib/issues/14298) -# But matplotlib 3.0 causes one in heatmaps -matplotlib>=3.1.2 -pandas>=0.21 -scipy>=1.4 -seaborn -h5py>=2.10.0 -tables -tqdm -importlib_metadata>=0.7; python_version < '3.8' -scikit-learn>=0.21.2 -statsmodels>=0.10.0rc2 -patsy -networkx>=2.3 -natsort -joblib -numba>=0.41.0 -umap-learn>=0.3.10 -legacy-api-wrap -packaging -sinfo diff --git a/scanpy/__init__.py b/scanpy/__init__.py index 71eb75e977..fa1124b85e 100644 --- a/scanpy/__init__.py +++ b/scanpy/__init__.py @@ -1,43 +1,42 @@ """Single-Cell Analysis in Python.""" -from ._metadata import __version__, __author__, __email__ - -from ._utils import check_versions - -check_versions() -del check_versions - -# the actual API -from ._settings import ( - settings, - Verbosity, -) # start with settings as several tools are using it -from . import tools as tl -from . import preprocessing as pp -from . import plotting as pl -from . import datasets, logging, queries, external, get - -from anndata import AnnData, concat -from anndata import ( - read_h5ad, - read_csv, - read_excel, - read_hdf, - read_loom, - read_mtx, - read_text, - read_umi_tools, -) -from .readwrite import read, read_10x_h5, read_10x_mtx, write, read_visium -from .neighbors import Neighbors - -set_figure_params = settings.set_figure_params - -# has to be done at the end, after everything has been imported -import sys - -sys.modules.update({f'{__name__}.{m}': globals()[m] for m in ['tl', 'pp', 'pl']}) -from ._utils import annotate_doc_types - -annotate_doc_types(sys.modules[__name__], 'scanpy') -del sys, annotate_doc_types +from ._metadata import __version__, __author__, __email__, within_flit + +if not within_flit(): # see function docstring on why this is there + from ._utils import check_versions + + check_versions() + del check_versions, within_flit + + # the actual API + # (start with settings as several tools are using it) + from ._settings import settings, Verbosity + from . import tools as tl + from . import preprocessing as pp + from . import plotting as pl + from . import datasets, logging, queries, external, get + + from anndata import AnnData, concat + from anndata import ( + read_h5ad, + read_csv, + read_excel, + read_hdf, + read_loom, + read_mtx, + read_text, + read_umi_tools, + ) + from .readwrite import read, read_10x_h5, read_10x_mtx, write, read_visium + from .neighbors import Neighbors + + set_figure_params = settings.set_figure_params + + # has to be done at the end, after everything has been imported + import sys + + sys.modules.update({f'{__name__}.{m}': globals()[m] for m in ['tl', 'pp', 'pl']}) + from ._utils import annotate_doc_types + + annotate_doc_types(sys.modules[__name__], 'scanpy') + del sys, annotate_doc_types diff --git a/scanpy/_metadata.py b/scanpy/_metadata.py index fda12a4350..500c83f67e 100644 --- a/scanpy/_metadata.py +++ b/scanpy/_metadata.py @@ -1,14 +1,34 @@ +import traceback from pathlib import Path here = Path(__file__).parent + +def refresh_entry_points(): + """\ + Under some circumstances, (e.g. when installing a PEP 517 package via pip), + pkg_resources.working_set.entries is stale. This tries to fix that. + See https://github.com/pypa/setuptools_scm/issues/513 + """ + try: + import sys + import pkg_resources + + ws: pkg_resources.WorkingSet = pkg_resources.working_set + for entry in sys.path: + ws.add_entry(entry) + except Exception: + pass + + try: from setuptools_scm import get_version import pytoml proj = pytoml.loads((here.parent / 'pyproject.toml').read_text()) - metadata = proj['tool']['scanpy'] + metadata = proj['tool']['flit']['metadata'] + refresh_entry_points() __version__ = get_version(root='..', relative_to=__file__) __author__ = metadata['author'] __email__ = metadata['author-email'] @@ -19,3 +39,16 @@ __version__ = metadata['Version'] __author__ = metadata['Author'] __email__ = metadata['Author-email'] + + +def within_flit(): + """\ + Checks if we are being imported by flit. + This is necessary so flit can import __version__ without all depedencies installed. + There are a few options to make this hack unnecessary, see: + https://github.com/takluyver/flit/issues/253#issuecomment-737870438 + """ + for frame in traceback.extract_stack(): + if frame.name == 'get_docstring_and_version_via_import': + return True + return False diff --git a/scanpy/_utils.py b/scanpy/_utils.py index 85bd0fdf8e..a2749c3e1b 100644 --- a/scanpy/_utils.py +++ b/scanpy/_utils.py @@ -141,6 +141,9 @@ def descend_classes_and_funcs(mod: ModuleType, root: str, encountered=None): if callable(m) and _one_of_ours(m, root): yield m elif isinstance(obj, ModuleType) and obj not in encountered: + if obj.__name__.startswith('scanpy.tests'): + # Python’s import mechanism seems to add this to `scanpy`’s attributes + continue encountered.add(obj) yield from descend_classes_and_funcs(obj, root, encountered) diff --git a/scanpy/tests/test_docs.py b/scanpy/tests/test_package_structure.py similarity index 67% rename from scanpy/tests/test_docs.py rename to scanpy/tests/test_package_structure.py index 0c82178be5..8c87d91b6d 100644 --- a/scanpy/tests/test_docs.py +++ b/scanpy/tests/test_package_structure.py @@ -1,5 +1,8 @@ +import email import inspect +import os from types import FunctionType +from pathlib import Path import pytest from scanpy._utils import descend_classes_and_funcs @@ -8,6 +11,9 @@ import scanpy.cli +mod_dir = Path(scanpy.__file__).parent +proj_dir = mod_dir.parent + scanpy_functions = [ c_or_f for c_or_f in descend_classes_and_funcs(scanpy, "scanpy") @@ -15,6 +21,16 @@ ] +@pytest.fixture +def in_project_dir(): + wd_orig = Path.cwd() + os.chdir(proj_dir) + try: + yield proj_dir + finally: + os.chdir(wd_orig) + + @pytest.mark.parametrize("f", scanpy_functions) def test_function_headers(f): name = f"{f.__module__}.{f.__qualname__}" @@ -41,5 +57,11 @@ def test_function_headers(f): raise SyntaxError(msg, (filename, lineno, 2, text)) -def test_plot_doc_signatures(): - pass +def test_metadata(tmp_path, in_project_dir): + import flit_core.buildapi + + flit_core.buildapi.prepare_metadata_for_build_wheel(tmp_path) + + metadata_path = next(tmp_path.glob('*.dist-info')) / 'METADATA' + metadata = email.message_from_bytes(metadata_path.read_bytes()) + assert not metadata.defects diff --git a/setup.py b/setup.py deleted file mode 100644 index cf0e22bf02..0000000000 --- a/setup.py +++ /dev/null @@ -1,82 +0,0 @@ -import sys - -if sys.version_info < (3, 6): - sys.exit('scanpy requires Python >= 3.6') -from pathlib import Path - -from setuptools import setup, find_packages - -try: - import pytoml -except ImportError: - sys.exit('Please use `pip install .` or install pytoml first.') - -proj = pytoml.loads(Path('pyproject.toml').read_text()) -metadata = proj['tool']['scanpy'] - -setup( - name='scanpy', - use_scm_version=True, - setup_requires=['setuptools_scm'], - description='Single-Cell Analysis in Python.', - long_description=Path('README.rst').read_text('utf-8'), - url='http://github.com/theislab/scanpy', - author=metadata['author'], - author_email=metadata['author-email'], - license='BSD', - python_requires='>=3.6', - install_requires=[ - l.strip() for l in Path('requirements.txt').read_text('utf-8').splitlines() - ], - extras_require=dict( - louvain=['python-igraph', 'louvain>=0.6,!=0.6.2'], - leiden=['python-igraph', 'leidenalg'], - bbknn=['bbknn'], - scvi=['scvi==0.6.7'], - rapids=['cudf>=0.9', 'cuml>=0.9', 'cugraph>=0.9'], - magic=['magic-impute>=2.0'], - skmisc=['scikit-misc>=0.1.3'], - harmony=['harmonypy'], - scanorama=['scanorama'], - scrublet=['scrublet'], - dev=['setuptools_scm', 'pytoml', 'black>=20.8b1'], - doc=[ - 'sphinx>=3.2', - 'sphinx_rtd_theme>=0.3.1', - 'readthedocs-sphinx-search', - 'sphinx_autodoc_typehints', - 'scanpydoc>=0.5', - 'typing_extensions; python_version < "3.8"', # for `Literal` - ], - test=[ - 'pytest>=4.4', - 'pytest-nunit', - 'dask[array]!=2.17.0', - 'fsspec', - 'zappy', - 'zarr', - 'profimp', - ], - ), - packages=find_packages(), - include_package_data=True, - entry_points=dict(console_scripts=['scanpy=scanpy.cli:console_main']), - zip_safe=False, - classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Environment :: Console', - 'Framework :: Jupyter', - 'Intended Audience :: Developers', - 'Intended Audience :: Science/Research', - 'Natural Language :: English', - 'Operating System :: MacOS :: MacOS X', - 'Operating System :: Microsoft :: Windows', - 'Operating System :: POSIX :: Linux', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Topic :: Scientific/Engineering :: Bio-Informatics', - 'Topic :: Scientific/Engineering :: Visualization', - ], -) From 364f32033ff00821a369c38bd14353c8b207ee3b Mon Sep 17 00:00:00 2001 From: Philipp A Date: Fri, 15 Jan 2021 15:51:45 +0100 Subject: [PATCH 22/85] add setup.py while leaving it ignored --- .gitignore | 1 - pyproject.toml | 7 ++++- setup.py | 72 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 setup.py diff --git a/.gitignore b/.gitignore index baa3c9b7f1..079c5b0cea 100644 --- a/.gitignore +++ b/.gitignore @@ -33,7 +33,6 @@ __pycache__/ /scanpy.egg-info/ /*-env/ /env-*/ -/setup.py # OS stuff .DS_Store diff --git a/pyproject.toml b/pyproject.toml index 901f1dd7e1..2ef0aba357 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,6 +5,8 @@ requires = [ 'setuptools_scm', 'pytoml', 'importlib_metadata>=0.7; python_version < "3.8"', + # setup.py stuff + 'packaging', ] [tool.flit.metadata] @@ -108,7 +110,10 @@ test = [ scanpy = 'scanpy.cli:console_main' [tool.flit.sdist] -exclude = ['scanpy/tests'] +exclude = [ + 'scanpy/tests', + 'setup.py', +] [tool.pytest.ini_options] python_files = 'test_*.py' diff --git a/setup.py b/setup.py new file mode 100644 index 0000000000..458fff1899 --- /dev/null +++ b/setup.py @@ -0,0 +1,72 @@ +"""Temporary setuptools bridge +Don't use this except if you have a deadline or you encounter a bug. +""" +import re +import sys +from warnings import warn + +import setuptools +from pathlib import Path + +from flit_core import common, config +from setuptools_scm.integration import find_files + + +field_map = dict( + description="summary", + long_description="description", + long_description_content_type="description_content_type", + python_requires="requires_python", + url="home_page", + **{ + n: n + for n in ["name", "version", "author", "author_email", "license", "classifiers"] + }, +) + + +def setup_args(config_path=Path("pyproject.toml")): + cfg = config.read_flit_config(config_path) + module = common.Module(cfg.module, config_path.parent) + metadata = common.make_metadata(module, cfg) + kwargs = {} + for st_field, metadata_field in field_map.items(): + val = getattr(metadata, metadata_field, None) + if val is not None: + kwargs[st_field] = val + else: + msg = f"{metadata_field} not found in {dir(metadata)}" + assert metadata_field in {"license"}, msg + kwargs["packages"] = setuptools.find_packages(include=[metadata.name + "*"]) + if metadata.requires_dist: + kwargs["install_requires"] = [ + req for req in metadata.requires_dist if "extra ==" not in req + ] + if cfg.reqs_by_extra: + kwargs["extras_require"] = cfg.reqs_by_extra + scripts = cfg.entrypoints.get("console_scripts") + if scripts is not None: + kwargs["entry_points"] = dict( + console_scipts=[" = ".join(ep) for ep in scripts.items()] + ) + kwargs["include_package_data"] = True + kwargs["package_data"] = { + module.name: [ + re.escape(f[len(module.name) + 1 :]) for f in find_files(module.path) + ] + } + return kwargs + + +if __name__ == "__main__": + if "develop" in sys.argv: + msg = ( + "Please use `flit install -s` or `flit install --pth-file` " + "instead of `pip install -e`/`python setup.py develop`" + ) + elif "install" in sys.argv: + msg = 'Please use `pip install "$d"` instead of `python "$d/setup.py" install`' + else: + msg = "Please use `pip ...` or `flit ...` instead of `python setup.py ...`" + warn(msg, FutureWarning) + setuptools.setup(**setup_args()) From 8f4f87ea7844695a63cabc2515cba9b65cf3e190 Mon Sep 17 00:00:00 2001 From: Philipp A Date: Thu, 14 Jan 2021 12:23:10 +0100 Subject: [PATCH 23/85] Update install instructions --- docs/installation.rst | 24 +++++++++++++----------- setup.py | 3 ++- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index b51f9012eb..4ef33bf0af 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -35,16 +35,20 @@ To work with the latest version `on GitHub`_: clone the repository and `cd` into its root directory. To install using symbolic links (stay up to date with your cloned version after you update with `git pull`) call:: - flit install -s # from an activated venv or conda env + flit install -s --deps=develop # from an activated venv or conda env # or - flit install -s --python path/to/venv/bin/python + flit install -s --deps=develop --python path/to/venv/bin/python -If you intend to do development work, there are some extra dependencies you'll want. -These can be install with `scanpy` via:: +.. _on GitHub: https://github.com/theislab/scanpy - pip install -e ".[dev,doc,test]" +.. note:: -.. _on GitHub: https://github.com/theislab/scanpy + Due to a `bug in pip`_, packages installed by `flit` can be uninstalled by normal pip operations. + For now, you can avoid this by using:: + + pip install -e ".[dev,doc,test]" + +.. _bug in pip: https://github.com/pypa/pip/issues/9670 If you want to let conda_ handle the installations of dependencies, do:: @@ -52,21 +56,19 @@ If you want to let conda_ handle the installations of dependencies, do:: beni pyproject.toml > environment.yml conda env create -f environment.yml conda activate scanpy - flit install -s + flit install -s --deps=develop # or: pip install -e ".[dev,doc,test]" On Windows, you might have to use `flit install --pth-file` if you are not able to give yourself the `create symbolic links`_ privilege. -Be aware that a `conda bug`_ might prevent `conda list` from working then. .. _create symbolic links: https://docs.microsoft.com/en-us/windows/security/threat-protection/security-policy-settings/create-symbolic-links -.. _conda bug: https://github.com/conda/conda/issues/9074 Docker ~~~~~~ -If you're using Docker_, you can use the minimal `fastgenomics/scanpy`_ image from the Docker Hub. +If you're using Docker_, you can use e.g. the image `gcfntnu/scanpy`_ from Docker Hub. .. _Docker: https://en.wikipedia.org/wiki/Docker_(software) -.. _fastgenomics/scanpy: https://hub.docker.com/r/fastgenomics/scanpy +.. _gcfntnu/scanpy: https://hub.docker.com/r/gcfntnu/scanpy .. _bioconda: https://bioconda.github.io/ Troubleshooting diff --git a/setup.py b/setup.py index 458fff1899..244084d7a0 100644 --- a/setup.py +++ b/setup.py @@ -62,7 +62,8 @@ def setup_args(config_path=Path("pyproject.toml")): if "develop" in sys.argv: msg = ( "Please use `flit install -s` or `flit install --pth-file` " - "instead of `pip install -e`/`python setup.py develop`" + "instead of `pip install -e`/`python setup.py develop` " + "once https://github.com/pypa/pip/issues/9670 is fixed." ) elif "install" in sys.argv: msg = 'Please use `pip install "$d"` instead of `python "$d/setup.py" install`' From d4f7d4c1fb4435a1b2ee10aa322365d7c102b493 Mon Sep 17 00:00:00 2001 From: Philipp A Date: Thu, 11 Feb 2021 11:27:18 +0100 Subject: [PATCH 24/85] Circumvent new pip check (see pypa/pip#9628) --- .azure-pipelines.yml | 2 ++ .pip-2033.txt | 4 ++++ .readthedocs.yml | 1 + 3 files changed, 7 insertions(+) create mode 100644 .pip-2033.txt diff --git a/.azure-pipelines.yml b/.azure-pipelines.yml index 02970df9e1..2019573954 100644 --- a/.azure-pipelines.yml +++ b/.azure-pipelines.yml @@ -42,6 +42,7 @@ jobs: - script: | python -m pip install --upgrade pip + pip install -r .pip-2033.txt pip install pytest-cov wheel pip install .[dev,doc,test,louvain,leiden,magic,scvi,harmony,scrublet,scanorama] displayName: 'Install dependencies' @@ -102,6 +103,7 @@ jobs: displayName: 'Display installed versions' - script: | + pip install flit flit build twine check dist/* displayName: 'Build & Twine check' diff --git a/.pip-2033.txt b/.pip-2033.txt new file mode 100644 index 0000000000..3c5aa646e0 --- /dev/null +++ b/.pip-2033.txt @@ -0,0 +1,4 @@ +# Flit mangles the +local part of the version spec in compliance with PEP 427, +# but pip 20.3.4 started expecting wheel filenames to contain the local part unmangled. +# https://github.com/pypa/pip/issues/9628 +pip==20.3.3 diff --git a/.readthedocs.yml b/.readthedocs.yml index 58e2487826..de089915c5 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -6,6 +6,7 @@ sphinx: python: version: 3.8 install: + - requirements: .pip-2033.txt - method: pip path: . extra_requirements: From 3db4814a809242b16f8bfaa8599d6564a485d366 Mon Sep 17 00:00:00 2001 From: Philipp A Date: Tue, 2 Mar 2021 12:03:15 +0100 Subject: [PATCH 25/85] Go back to regular pip (#1702) * Go back to regular flit Co-authored-by: Isaac Virshup --- .azure-pipelines.yml | 8 +++----- .pip-2033.txt | 4 ---- .readthedocs.yml | 1 - docs/installation.rst | 15 +++++---------- pyproject.toml | 2 +- setup.py | 3 +-- 6 files changed, 10 insertions(+), 23 deletions(-) delete mode 100644 .pip-2033.txt diff --git a/.azure-pipelines.yml b/.azure-pipelines.yml index 2019573954..41bedee83a 100644 --- a/.azure-pipelines.yml +++ b/.azure-pipelines.yml @@ -42,7 +42,6 @@ jobs: - script: | python -m pip install --upgrade pip - pip install -r .pip-2033.txt pip install pytest-cov wheel pip install .[dev,doc,test,louvain,leiden,magic,scvi,harmony,scrublet,scanorama] displayName: 'Install dependencies' @@ -96,14 +95,13 @@ jobs: - script: | python -m pip install --upgrade pip - pip install setuptools setuptools_scm pytoml wheel twine - displayName: 'Install build dependencies' + pip install build twine + displayName: 'Install build tools and requirements' - script: pip list displayName: 'Display installed versions' - script: | - pip install flit - flit build + python -m build --sdist --wheel . twine check dist/* displayName: 'Build & Twine check' diff --git a/.pip-2033.txt b/.pip-2033.txt deleted file mode 100644 index 3c5aa646e0..0000000000 --- a/.pip-2033.txt +++ /dev/null @@ -1,4 +0,0 @@ -# Flit mangles the +local part of the version spec in compliance with PEP 427, -# but pip 20.3.4 started expecting wheel filenames to contain the local part unmangled. -# https://github.com/pypa/pip/issues/9628 -pip==20.3.3 diff --git a/.readthedocs.yml b/.readthedocs.yml index de089915c5..58e2487826 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -6,7 +6,6 @@ sphinx: python: version: 3.8 install: - - requirements: .pip-2033.txt - method: pip path: . extra_requirements: diff --git a/docs/installation.rst b/docs/installation.rst index 4ef33bf0af..67f06cf9f7 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -41,28 +41,23 @@ cloned version after you update with `git pull`) call:: .. _on GitHub: https://github.com/theislab/scanpy -.. note:: - - Due to a `bug in pip`_, packages installed by `flit` can be uninstalled by normal pip operations. - For now, you can avoid this by using:: - - pip install -e ".[dev,doc,test]" - -.. _bug in pip: https://github.com/pypa/pip/issues/9670 - If you want to let conda_ handle the installations of dependencies, do:: pip install beni beni pyproject.toml > environment.yml conda env create -f environment.yml conda activate scanpy - flit install -s --deps=develop # or: pip install -e ".[dev,doc,test]" + flit install -s --deps=develop On Windows, you might have to use `flit install --pth-file` if you are not able to give yourself the `create symbolic links`_ privilege. .. _create symbolic links: https://docs.microsoft.com/en-us/windows/security/threat-protection/security-policy-settings/create-symbolic-links +.. note:: + + `pip install -e` still works, but may not in future versions. + Docker ~~~~~~ If you're using Docker_, you can use e.g. the image `gcfntnu/scanpy`_ from Docker Hub. diff --git a/pyproject.toml b/pyproject.toml index 2ef0aba357..fe34eca22d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [build-system] build-backend = 'flit_core.buildapi' requires = [ - 'flit_core >=2,<4', + 'flit_core >=3.1,<4', 'setuptools_scm', 'pytoml', 'importlib_metadata>=0.7; python_version < "3.8"', diff --git a/setup.py b/setup.py index 244084d7a0..458fff1899 100644 --- a/setup.py +++ b/setup.py @@ -62,8 +62,7 @@ def setup_args(config_path=Path("pyproject.toml")): if "develop" in sys.argv: msg = ( "Please use `flit install -s` or `flit install --pth-file` " - "instead of `pip install -e`/`python setup.py develop` " - "once https://github.com/pypa/pip/issues/9670 is fixed." + "instead of `pip install -e`/`python setup.py develop`" ) elif "install" in sys.argv: msg = 'Please use `pip install "$d"` instead of `python "$d/setup.py" install`' From 6a97d73925ca998535a8b0445b71a4f6afc5602f Mon Sep 17 00:00:00 2001 From: Isaac Virshup Date: Wed, 3 Mar 2021 10:47:22 +1100 Subject: [PATCH 26/85] codecov comment (#1704) --- .codecov.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.codecov.yml b/.codecov.yml index 8e49cc3831..9dd8f244af 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -1,6 +1,6 @@ # Based on pydata/xarray codecov: - require_ci_to_pass: yes + require_ci_to_pass: no coverage: status: @@ -11,4 +11,7 @@ coverage: patch: false changes: false -comment: off \ No newline at end of file +comment: + layout: "diff, flags, files" + behavior: once + require_base: no From 47af6310f1a9bc9fad7b8de85180ee22a8adc963 Mon Sep 17 00:00:00 2001 From: Isaac Virshup Date: Wed, 3 Mar 2021 20:29:48 +1100 Subject: [PATCH 27/85] Use joblib for parallelism in regress_out (#1695) * Use joblib for parallism in regress_out * release note * fix link in release notes * Add todo for resource test --- docs/release-notes/1.7.2.rst | 1 + scanpy/preprocessing/_simple.py | 11 +++-------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/docs/release-notes/1.7.2.rst b/docs/release-notes/1.7.2.rst index 029c2eb746..e2c4a8967d 100644 --- a/docs/release-notes/1.7.2.rst +++ b/docs/release-notes/1.7.2.rst @@ -7,6 +7,7 @@ - Fix :func:`scanpy.pl.paga_path` `TypeError` with recent versions of anndata :pr:`1047` :smaller:`P Angerer` - :func:`scanpy.logging.print_versions` now works when `python<3.8` :pr:`1691` :smaller:`I Virshup` +- :func:`scanpy.pp.regress_out` now uses `joblib` as the parallel backend, and should stop oversubscribing threads :pr:`1694` :smaller:`I Virshup` .. rubric:: Deprecations diff --git a/scanpy/preprocessing/_simple.py b/scanpy/preprocessing/_simple.py index cae9e7c581..344ea2022c 100644 --- a/scanpy/preprocessing/_simple.py +++ b/scanpy/preprocessing/_simple.py @@ -662,15 +662,10 @@ def regress_out( regres = regressors tasks.append(tuple((data_chunk, regres, variable_is_categorical))) - if n_jobs > 1 and n_chunks > 1: - import multiprocessing + from joblib import Parallel, delayed - pool = multiprocessing.Pool(n_jobs) - res = pool.map_async(_regress_out_chunk, tasks).get(9999999) - pool.close() - - else: - res = list(map(_regress_out_chunk, tasks)) + # TODO: figure out how to test that this doesn't oversubscribe resources + res = Parallel(n_jobs=n_jobs)(delayed(_regress_out_chunk)(task) for task in tasks) # res is a list of vectors (each corresponding to a regressed gene column). # The transpose is needed to get the matrix in the shape needed From 6d36c6b12bcd1987a1c29ff7cdc2402c3904a564 Mon Sep 17 00:00:00 2001 From: Jonathan Manning Date: Wed, 3 Mar 2021 10:09:08 +0000 Subject: [PATCH 28/85] Add sparsificiation step before sparse-dependent Scrublet calls (#1707) * Add sparsificiation step before sparse-dependent Scrublet calls * Apply sparsification suggestion Co-authored-by: Isaac Virshup * Fix imports Co-authored-by: Isaac Virshup --- scanpy/external/pp/_scrublet.py | 13 +++++++++++-- scanpy/tests/external/test_scrublet.py | 20 ++++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/scanpy/external/pp/_scrublet.py b/scanpy/external/pp/_scrublet.py index 1965bb0f88..6611fccac5 100644 --- a/scanpy/external/pp/_scrublet.py +++ b/scanpy/external/pp/_scrublet.py @@ -1,6 +1,8 @@ from anndata import AnnData from typing import Optional import numpy as np +from scipy import sparse + from ... import logging as logg from ... import preprocessing as pp @@ -349,6 +351,9 @@ def _scrublet_call_doublets( if n_neighbors is None: n_neighbors = int(round(0.5 * np.sqrt(adata_obs.shape[0]))) + # Note: Scrublet() will sparse adata_obs.X if it's not already, but this + # matrix won't get used if we pre-set the normalised slots. + scrub = sl.Scrublet( adata_obs.X, n_neighbors=n_neighbors, @@ -357,8 +362,12 @@ def _scrublet_call_doublets( random_state=random_state, ) - scrub._E_obs_norm = adata_obs.X - scrub._E_sim_norm = adata_sim.X + # Ensure normalised matrix sparseness as Scrublet does + # https://github.com/swolock/scrublet/blob/67f8ecbad14e8e1aa9c89b43dac6638cebe38640/src/scrublet/scrublet.py#L100 + + scrub._E_obs_norm = sparse.csc_matrix(adata_obs.X) + scrub._E_sim_norm = sparse.csc_matrix(adata_sim.X) + scrub.doublet_parents_ = adata_sim.obsm['doublet_parents'] # Call scrublet-specific preprocessing where specified diff --git a/scanpy/tests/external/test_scrublet.py b/scanpy/tests/external/test_scrublet.py index b67b479979..bbdf747b14 100644 --- a/scanpy/tests/external/test_scrublet.py +++ b/scanpy/tests/external/test_scrublet.py @@ -23,6 +23,26 @@ def test_scrublet(): assert adata.obs["predicted_doublet"].any(), "Expect some doublets to be identified" +def test_scrublet_dense(): + """ + Test that Scrublet works for dense matrices. + + Check that scrublet runs and detects some doublets when a dense matrix is supplied. + """ + pytest.importorskip("scrublet") + + adata = sc.datasets.paul15()[:500].copy() + sce.pp.scrublet(adata, use_approx_neighbors=False) + + errors = [] + + # replace assertions by conditions + assert "predicted_doublet" in adata.obs.columns + assert "doublet_score" in adata.obs.columns + + assert adata.obs["predicted_doublet"].any(), "Expect some doublets to be identified" + + def test_scrublet_params(): """ Test that Scrublet args are passed. From c7bd6dc1a07ead12e510a080e8099c66f5fcd8c9 Mon Sep 17 00:00:00 2001 From: Philipp A Date: Wed, 3 Mar 2021 13:18:48 +0100 Subject: [PATCH 29/85] Fix version on Travis (#1713) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit By default, Travis does `git clone --depth=50` which means the version can’t be detected from the git tag. --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index cb5c7a96eb..b8cd6c2ff2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,7 @@ dist: xenial language: python +git: + depth: false # Version detection needs all info branches: only: - master # All other branches should become (draft) PRs and be build that way From 4eb64c2a1f7a36db7fbaebc05145528466fab041 Mon Sep 17 00:00:00 2001 From: Isaac Virshup Date: Thu, 4 Mar 2021 17:18:33 +1100 Subject: [PATCH 30/85] `sc.metrics` module (add confusion matrix & Geary's C methods) (#915) * Add `sc.metrics` with `gearys_c` Add a module for computing useful metrics. Started off with Geary's C since I'm using it and finding it useful. I've also got a fairly fast way to calculate it worked out. Unfortunatly my implementation runs into some issues with some global configs set by umap (see https://github.com/lmcinnes/umap/issues/306), so I'm going to see if that can be resolved before changing it. * Add sc.metrics.confusion_matrix * Better tests and output for confusion_matrix * Workaround umap<0.4 and increase numerical stability of gearys_c * Work around https://github.com/lmcinnes/umap/issues/306 by not calling out to kernel function. That code has been kept, but commented out. * Increase numerical stability by casting data to system width. Tests were failing due to instability. * Split up gearys_c tests * Improved unexpected error message * gearys_c working again. Sadly, a bit slower * One option for doc strings * Simplify implementation to use single dispatch * release notes --- docs/api/index.rst | 15 ++ docs/conf.py | 1 + docs/release-notes/1.8.0.rst | 7 + scanpy/__init__.py | 2 +- scanpy/metrics/__init__.py | 2 + scanpy/metrics/_gearys_c.py | 298 +++++++++++++++++++++++++ scanpy/metrics/_metrics.py | 88 ++++++++ scanpy/tests/test_metrics.py | 116 ++++++++++ scanpy/tests/test_package_structure.py | 1 - 9 files changed, 528 insertions(+), 2 deletions(-) create mode 100644 scanpy/metrics/__init__.py create mode 100644 scanpy/metrics/_gearys_c.py create mode 100644 scanpy/metrics/_metrics.py create mode 100644 scanpy/tests/test_metrics.py diff --git a/docs/api/index.rst b/docs/api/index.rst index 5ca53b0028..e99bb0a8dd 100644 --- a/docs/api/index.rst +++ b/docs/api/index.rst @@ -236,6 +236,21 @@ This module provides useful queries for annotation and enrichment. queries.enrich +Metrics +------- + +.. module:: scanpy.metrics +.. currentmodule:: scanpy + +Collections of useful measurements for evaluating results. + +.. autosummary:: + :toctree: . + + metrics.confusion_matrix + metrics.gearys_c + + Classes ------- diff --git a/docs/conf.py b/docs/conf.py index 2e2b3c5157..46553c1640 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -139,6 +139,7 @@ def setup(app): "scanpy.plotting._matrixplot.MatrixPlot": "scanpy.pl.MatrixPlot", "scanpy.plotting._dotplot.DotPlot": "scanpy.pl.DotPlot", "scanpy.plotting._stacked_violin.StackedViolin": "scanpy.pl.StackedViolin", + "pandas.core.series.Series": "pandas.Series", } nitpick_ignore = [ diff --git a/docs/release-notes/1.8.0.rst b/docs/release-notes/1.8.0.rst index b98676558a..f3759523e0 100644 --- a/docs/release-notes/1.8.0.rst +++ b/docs/release-notes/1.8.0.rst @@ -8,6 +8,13 @@ .. _flit: https://flit.readthedocs.io/en/latest/ +.. rubric:: Metrics module + +- Added :mod:`scanpy.metrics` module! + + - Added :func:`scanpy.metrics.confusion_matrix` for comparing labellings :pr:`915` :smaller:`I Virshup` + - Added :func:`scanpy.metrics.gearys_c` for spatial autocorrelation :pr:`915` :smaller:`I Virshup` + .. rubric:: External tools .. rubric:: Performance enhancements diff --git a/scanpy/__init__.py b/scanpy/__init__.py index fa1124b85e..31b7c7dba9 100644 --- a/scanpy/__init__.py +++ b/scanpy/__init__.py @@ -14,7 +14,7 @@ from . import tools as tl from . import preprocessing as pp from . import plotting as pl - from . import datasets, logging, queries, external, get + from . import datasets, logging, queries, external, get, metrics from anndata import AnnData, concat from anndata import ( diff --git a/scanpy/metrics/__init__.py b/scanpy/metrics/__init__.py new file mode 100644 index 0000000000..d542d27dd4 --- /dev/null +++ b/scanpy/metrics/__init__.py @@ -0,0 +1,2 @@ +from ._gearys_c import gearys_c +from ._metrics import confusion_matrix diff --git a/scanpy/metrics/_gearys_c.py b/scanpy/metrics/_gearys_c.py new file mode 100644 index 0000000000..4d7f5dd618 --- /dev/null +++ b/scanpy/metrics/_gearys_c.py @@ -0,0 +1,298 @@ +from functools import singledispatch +from typing import Optional, Union + + +from anndata import AnnData +from scanpy.get import _get_obs_rep +import numba +import numpy as np +import pandas as pd +from scipy import sparse + + +@singledispatch +def gearys_c( + adata: AnnData, + *, + vals: Optional[Union[np.ndarray, sparse.spmatrix]] = None, + use_graph: Optional[str] = None, + layer: Optional[str] = None, + obsm: Optional[str] = None, + obsp: Optional[str] = None, + use_raw: bool = False, +) -> Union[np.ndarray, float]: + r""" + Calculate `Geary's C `_, as used + by `VISION `_. + + Geary's C is a measure of autocorrelation for some measure on a graph. This + can be to whether measures are correlated between neighboring cells. Lower + values indicate greater correlation. + + .. math:: + + C = + \frac{ + (N - 1)\sum_{i,j} w_{i,j} (x_i - x_j)^2 + }{ + 2W \sum_i (x_i - \bar{x})^2 + } + + Params + ------ + adata + vals + Values to calculate Geary's C for. If this is two dimensional, should + be of shape `(n_features, n_cells)`. Otherwise should be of shape + `(n_cells,)`. This matrix can be selected from elements of the anndata + object by using key word arguments: `layer`, `obsm`, `obsp`, or + `use_raw`. + use_graph + Key to use for graph in anndata object. If not provided, default + neighbors connectivities will be used instead. + layer + Key for `adata.layers` to choose `vals`. + obsm + Key for `adata.obsm` to choose `vals`. + obsp + Key for `adata.obsp` to choose `vals`. + use_raw + Whether to use `adata.raw.X` for `vals`. + + + This function can also be called on the graph and values directly. In this case + the signature looks like: + + Params + ------ + g + The graph + vals + The values + + + See the examples for more info. + + Returns + ------- + If vals is two dimensional, returns a 1 dimensional ndarray array. Returns + a scalar if `vals` is 1d. + + + Examples + -------- + + Calculate Gearys C for each components of a dimensionality reduction: + + .. code:: python + + import scanpy as sc, numpy as np + + pbmc = sc.datasets.pbmc68k_processed() + pc_c = sc.metrics.gearys_c(pbmc, obsm="X_pca") + + + It's equivalent to call the function directly on the underlying arrays: + + .. code:: python + + alt = sc.metrics.gearys_c(pbmc.obsp["connectivities"], pbmc.obsm["X_pca"].T) + np.testing.assert_array_equal(pc_c, alt) + """ + if use_graph is None: + # Fix for anndata<0.7 + if hasattr(adata, "obsp") and "connectivities" in adata.obsp: + g = adata.obsp["connectivities"] + elif "neighbors" in adata.uns: + g = adata.uns["neighbors"]["connectivities"] + else: + raise ValueError("Must run neighbors first.") + else: + raise NotImplementedError() + if vals is None: + vals = _get_obs_rep(adata, use_raw=use_raw, layer=layer, obsm=obsm, obsp=obsp).T + return gearys_c(g, vals) + + +############################################################################### +# Calculation +############################################################################### +# Some notes on the implementation: +# * This could be phrased as tensor multiplication. However that does not get +# parallelized, which boosts performance almost linearly with cores. +# * Due to the umap setting the default threading backend, a parallel numba +# function that calls another parallel numba function can get stuck. This +# ends up meaning code re-use will be limited until umap 0.4. +# See: https://github.com/lmcinnes/umap/issues/306 +# * There can be a fair amount of numerical instability here (big reductions), +# so data is cast to float64. Removing these casts/ conversion will cause the +# tests to fail. + + +@numba.njit(cache=True, parallel=True) +def _gearys_c_vec(data, indices, indptr, x): + W = data.sum() + return _gearys_c_vec_W(data, indices, indptr, x, W) + + +@numba.njit(cache=True, parallel=True) +def _gearys_c_vec_W(data, indices, indptr, x, W): + N = len(indptr) - 1 + x = x.astype(np.float_) + x_bar = x.mean() + + total = 0.0 + for i in numba.prange(N): + s = slice(indptr[i], indptr[i + 1]) + i_indices = indices[s] + i_data = data[s] + total += np.sum(i_data * ((x[i] - x[i_indices]) ** 2)) + + numer = (N - 1) * total + denom = 2 * W * ((x - x_bar) ** 2).sum() + C = numer / denom + + return C + + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# Inner functions (per element C) +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# For calling gearys_c on collections. +# TODO: These are faster if we can compile them in parallel mode. However, +# `workqueue` does not allow nested functions to be parallelized. +# Additionally, there are currently problems with numba's compiler around +# parallelization of this code: +# https://github.com/numba/numba/issues/6774#issuecomment-788789663 + + +@numba.njit +def _gearys_c_inner_sparse_x_densevec(g_data, g_indices, g_indptr, x, W): + x_bar = x.mean() + total = 0.0 + N = len(x) + for i in numba.prange(N): + s = slice(g_indptr[i], g_indptr[i + 1]) + i_indices = g_indices[s] + i_data = g_data[s] + total += np.sum(i_data * ((x[i] - x[i_indices]) ** 2)) + numer = (N - 1) * total + denom = 2 * W * ((x - x_bar) ** 2).sum() + C = numer / denom + return C + + +@numba.njit +def _gearys_c_inner_sparse_x_sparsevec( + g_data, g_indices, g_indptr, x_data, x_indices, N, W +): + x = np.zeros(N, dtype=np.float_) + x[x_indices] = x_data + x_bar = np.sum(x_data) / N + total = 0.0 + N = len(x) + for i in numba.prange(N): + s = slice(g_indptr[i], g_indptr[i + 1]) + i_indices = g_indices[s] + i_data = g_data[s] + total += np.sum(i_data * ((x[i] - x[i_indices]) ** 2)) + numer = (N - 1) * total + # Expanded from 2 * W * ((x_k - x_k_bar) ** 2).sum(), but uses sparsity + # to skip some calculations + # fmt: off + denom = ( + 2 * W + * ( + np.sum(x_data ** 2) + - np.sum(x_data * x_bar * 2) + + (x_bar ** 2) * N + ) + ) + # fmt: on + C = numer / denom + return C + + +@numba.njit(cache=True, parallel=True) +def _gearys_c_mtx(g_data, g_indices, g_indptr, X): + M, N = X.shape + assert N == len(g_indptr) - 1 + W = g_data.sum() + out = np.zeros(M, dtype=np.float_) + for k in numba.prange(M): + x = X[k, :].astype(np.float_) + out[k] = _gearys_c_inner_sparse_x_densevec(g_data, g_indices, g_indptr, x, W) + return out + + +@numba.njit(cache=True, parallel=True) +def _gearys_c_mtx_csr( + g_data, g_indices, g_indptr, x_data, x_indices, x_indptr, x_shape +): + M, N = x_shape + W = g_data.sum() + out = np.zeros(M, dtype=np.float_) + x_data_list = np.split(x_data, x_indptr[1:-1]) + x_indices_list = np.split(x_indices, x_indptr[1:-1]) + for k in numba.prange(M): + out[k] = _gearys_c_inner_sparse_x_sparsevec( + g_data, + g_indices, + g_indptr, + x_data_list[k], + x_indices_list[k], + N, + W, + ) + return out + + +############################################################################### +# Interface +############################################################################### +@singledispatch +def _resolve_vals(val): + return np.asarray(val) + + +@_resolve_vals.register(np.ndarray) +@_resolve_vals.register(sparse.csr_matrix) +def _(val): + return val + + +@_resolve_vals.register(sparse.spmatrix) +def _(val): + return sparse.csr_matrix(val) + + +@_resolve_vals.register(pd.DataFrame) +@_resolve_vals.register(pd.Series) +def _(val): + return val.to_numpy() + + +@gearys_c.register(sparse.csr_matrix) +def _gearys_c(g, vals) -> np.ndarray: + assert g.shape[0] == g.shape[1], "`g` should be a square adjacency matrix" + vals = _resolve_vals(vals) + g_data = g.data.astype(np.float_, copy=False) + if isinstance(vals, sparse.csr_matrix): + assert g.shape[0] == vals.shape[1] + return _gearys_c_mtx_csr( + g_data, + g.indices, + g.indptr, + vals.data.astype(np.float_, copy=False), + vals.indices, + vals.indptr, + vals.shape, + ) + elif isinstance(vals, np.ndarray) and vals.ndim == 1: + assert g.shape[0] == vals.shape[0] + return _gearys_c_vec(g_data, g.indices, g.indptr, vals) + elif isinstance(vals, np.ndarray) and vals.ndim == 2: + assert g.shape[0] == vals.shape[1] + return _gearys_c_mtx(g_data, g.indices, g.indptr, vals) + else: + raise NotImplementedError() diff --git a/scanpy/metrics/_metrics.py b/scanpy/metrics/_metrics.py new file mode 100644 index 0000000000..71edf36dfe --- /dev/null +++ b/scanpy/metrics/_metrics.py @@ -0,0 +1,88 @@ +""" +Metrics which don't quite deserve their own file. +""" +from typing import Optional, Sequence, Union + +import pandas as pd +from pandas.api.types import is_categorical +from natsort import natsorted +import numpy as np + + +def confusion_matrix( + orig: Union[pd.Series, np.ndarray, Sequence], + new: Union[pd.Series, np.ndarray, Sequence], + data: Optional[pd.DataFrame] = None, + *, + normalize: bool = True, +) -> pd.DataFrame: + """\ + Given an original and new set of labels, create a labelled confusion matrix. + + Parameters `orig` and `new` can either be entries in data or categorical arrays + of the same size. + + Params + ------ + orig + Original labels. + new + New labels. + data + Optional dataframe to fill entries from. + normalize + Should the confusion matrix be normalized? + + + Examples + -------- + + .. plot:: + + import scanpy as sc; import seaborn as sns + pbmc = sc.datasets.pbmc68k_reduced() + cmtx = sc.metrics.confusion_matrix("bulk_labels", "louvain", pbmc.obs) + sns.heatmap(cmtx) + + """ + from sklearn.metrics import confusion_matrix as _confusion_matrix + + if data is not None: + if isinstance(orig, str): + orig = data[orig] + if isinstance(new, str): + new = data[new] + + # Coercing so I don't have to deal with it later + orig, new = pd.Series(orig), pd.Series(new) + assert len(orig) == len(new) + + unique_labels = pd.unique(np.concatenate((orig.values, new.values))) + + # Compute + mtx = _confusion_matrix(orig, new, labels=unique_labels) + if normalize: + sums = mtx.sum(axis=1)[:, np.newaxis] + mtx = np.divide(mtx, sums, where=sums != 0) + + # Label + orig_name = "Original labels" if orig.name is None else orig.name + new_name = "New Labels" if new.name is None else new.name + df = pd.DataFrame( + mtx, + index=pd.Index(unique_labels, name=orig_name), + columns=pd.Index(unique_labels, name=new_name), + ) + + # Filter + if is_categorical(orig): + orig_idx = pd.Series(orig).cat.categories + else: + orig_idx = natsorted(pd.unique(orig)) + if is_categorical(new): + new_idx = pd.Series(new).cat.categories + else: + new_idx = natsorted(pd.unique(new)) + df = df.loc[np.array(orig_idx), np.array(new_idx)] + + return df diff --git a/scanpy/tests/test_metrics.py b/scanpy/tests/test_metrics.py new file mode 100644 index 0000000000..846226d1dc --- /dev/null +++ b/scanpy/tests/test_metrics.py @@ -0,0 +1,116 @@ +from operator import eq +from string import ascii_letters + +import numpy as np +import pandas as pd +import scanpy as sc +from scipy import sparse + + +def test_gearys_c_consistency(): + pbmc = sc.datasets.pbmc68k_reduced() + pbmc.layers["raw"] = pbmc.raw.X.copy() + g = pbmc.obsp["connectivities"] + + assert eq( + sc.metrics.gearys_c(g, pbmc.obs["percent_mito"]), + sc.metrics.gearys_c(pbmc, vals=pbmc.obs["percent_mito"]), + ) + + assert eq( # Test that series and vectors return same value + sc.metrics.gearys_c(g, pbmc.obs["percent_mito"]), + sc.metrics.gearys_c(g, pbmc.obs["percent_mito"].values), + ) + + np.testing.assert_array_equal( + sc.metrics.gearys_c(pbmc, obsm="X_pca"), + sc.metrics.gearys_c(g, pbmc.obsm["X_pca"].T), + ) + + all_genes = sc.metrics.gearys_c(pbmc, layer="raw") + first_gene = sc.metrics.gearys_c( + pbmc, vals=pbmc.obs_vector(pbmc.var_names[0], layer="raw") + ) + + np.testing.assert_allclose(all_genes[0], first_gene) + + np.testing.assert_allclose( + sc.metrics.gearys_c(pbmc, layer="raw"), + sc.metrics.gearys_c(pbmc, vals=pbmc.layers["raw"].T.toarray()), + ) + + +def test_gearys_c_correctness(): + # Test case with perfectly seperated groups + connected = np.zeros(100) + connected[np.random.choice(100, size=30, replace=False)] = 1 + graph = np.zeros((100, 100)) + graph[np.ix_(connected.astype(bool), connected.astype(bool))] = 1 + graph[np.ix_(~connected.astype(bool), ~connected.astype(bool))] = 1 + graph = sparse.csr_matrix(graph) + + assert sc.metrics.gearys_c(graph, connected) == 0.0 + assert eq( + sc.metrics.gearys_c(graph, connected), + sc.metrics.gearys_c(graph, sparse.csr_matrix(connected)), + ) + # Check for anndata > 0.7 + if hasattr(sc.AnnData, "obsp"): + # Checking that obsp works + adata = sc.AnnData( + sparse.csr_matrix((100, 100)), obsp={"connectivities": graph} + ) + assert sc.metrics.gearys_c(adata, vals=connected) == 0.0 + + +def test_confusion_matrix(): + mtx = sc.metrics.confusion_matrix(["a", "b"], ["c", "d"], normalize=False) + assert mtx.loc["a", "c"] == 1 + assert mtx.loc["a", "d"] == 0 + assert mtx.loc["b", "d"] == 1 + assert mtx.loc["b", "c"] == 0 + + mtx = sc.metrics.confusion_matrix(["a", "b"], ["c", "d"], normalize=True) + assert mtx.loc["a", "c"] == 1.0 + assert mtx.loc["a", "d"] == 0.0 + assert mtx.loc["b", "d"] == 1.0 + assert mtx.loc["b", "c"] == 0.0 + + mtx = sc.metrics.confusion_matrix( + ["a", "a", "b", "b"], ["c", "d", "c", "d"], normalize=True + ) + assert np.all(mtx == 0.5) + + +def test_confusion_matrix_randomized(): + chars = np.array(list(ascii_letters)) + pos = np.random.choice(len(chars), size=np.random.randint(50, 150)) + a = chars[pos] + b = np.random.permutation(chars)[pos] + df = pd.DataFrame({"a": a, "b": b}) + + pd.testing.assert_frame_equal( + sc.metrics.confusion_matrix("a", "b", df), + sc.metrics.confusion_matrix(df["a"], df["b"]), + ) + pd.testing.assert_frame_equal( + sc.metrics.confusion_matrix(df["a"].values, df["b"].values), + sc.metrics.confusion_matrix(a, b), + ) + + +def test_confusion_matrix_api(): + data = pd.DataFrame( + {"a": np.random.randint(5, size=100), "b": np.random.randint(5, size=100)} + ) + expected = sc.metrics.confusion_matrix(data["a"], data["b"]) + + pd.testing.assert_frame_equal(expected, sc.metrics.confusion_matrix("a", "b", data)) + + pd.testing.assert_frame_equal( + expected, sc.metrics.confusion_matrix("a", data["b"], data) + ) + + pd.testing.assert_frame_equal( + expected, sc.metrics.confusion_matrix(data["a"], "b", data) + ) diff --git a/scanpy/tests/test_package_structure.py b/scanpy/tests/test_package_structure.py index 8c87d91b6d..abaa8fc312 100644 --- a/scanpy/tests/test_package_structure.py +++ b/scanpy/tests/test_package_structure.py @@ -36,7 +36,6 @@ def test_function_headers(f): name = f"{f.__module__}.{f.__qualname__}" assert f.__doc__ is not None, f"{name} has no docstring" lines = getattr(f, "__orig_doc__", f.__doc__).split("\n") - assert lines[0], f"{name} needs a single-line summary" broken = [i for i, l in enumerate(lines) if l.strip() and not l.startswith(" ")] if any(broken): msg = f'''\ From c11c486bbd3bd3a2ad4b09adf10bd2a46c8a1afa Mon Sep 17 00:00:00 2001 From: Isaac Virshup Date: Thu, 4 Mar 2021 18:28:31 +1100 Subject: [PATCH 31/85] Fix clipped images in docs (#1717) --- docs/matplotlibrc | 1 + 1 file changed, 1 insertion(+) create mode 100644 docs/matplotlibrc diff --git a/docs/matplotlibrc b/docs/matplotlibrc new file mode 100644 index 0000000000..67a95bbfd0 --- /dev/null +++ b/docs/matplotlibrc @@ -0,0 +1 @@ +savefig.bbox : tight From f637c08ea30f6119771843a1eda38a698ee6359f Mon Sep 17 00:00:00 2001 From: Isaac Virshup Date: Fri, 5 Mar 2021 15:18:21 +1100 Subject: [PATCH 32/85] Cleanup normalize_total (#1667) * Cleanup normalize_total * Add modification tests and copy kwarg for normalize_total * Test that 'layers' argument is deprecated * Added more mutation checks for normalize_total * release note * Error message --- docs/release-notes/1.8.0.rst | 3 + scanpy/preprocessing/_normalization.py | 104 +++++++++++++++---------- scanpy/tests/helpers.py | 94 ++++++++++++---------- scanpy/tests/test_normalization.py | 16 +++- 4 files changed, 134 insertions(+), 83 deletions(-) diff --git a/docs/release-notes/1.8.0.rst b/docs/release-notes/1.8.0.rst index f3759523e0..31f53e6f47 100644 --- a/docs/release-notes/1.8.0.rst +++ b/docs/release-notes/1.8.0.rst @@ -5,6 +5,7 @@ - Switched to flit_ for building and deploying the package, a simple tool with an easy to understand command line interface and metadata. +- Added `layer` and `copy` kwargs to :func:`~scanpy.pp.normalize_total` :pr:`1667` :smaller:`I Virshup` .. _flit: https://flit.readthedocs.io/en/latest/ @@ -22,3 +23,5 @@ .. rubric:: Bug fixes .. rubric:: Deprecations + +- Deprecated `layers` and `layers_norm` kwargs to :func:`~scanpy.pp.normalize_total` :pr:`1667` :smaller:`I Virshup` diff --git a/scanpy/preprocessing/_normalization.py b/scanpy/preprocessing/_normalization.py index 1931961693..be78bdb0c8 100644 --- a/scanpy/preprocessing/_normalization.py +++ b/scanpy/preprocessing/_normalization.py @@ -1,4 +1,5 @@ from typing import Optional, Union, Iterable, Dict +from warnings import warn import numpy as np from anndata import AnnData @@ -8,6 +9,7 @@ from .. import logging as logg from .._compat import Literal from .._utils import view_to_actual +from scanpy.get import _get_obs_rep, _set_obs_rep def _normalize_data(X, counts, after=None, copy=False): @@ -31,9 +33,11 @@ def normalize_total( exclude_highly_expressed: bool = False, max_fraction: float = 0.05, key_added: Optional[str] = None, + layer: Optional[str] = None, layers: Union[Literal['all'], Iterable[str]] = None, layer_norm: Optional[str] = None, inplace: bool = True, + copy: bool = False, ) -> Optional[Dict[str, np.ndarray]]: """\ Normalize counts per cell. @@ -72,23 +76,13 @@ def normalize_total( key_added Name of the field in `adata.obs` where the normalization factor is stored. - layers - List of layers to normalize. Set to `'all'` to normalize all layers. - layer_norm - Specifies how to normalize layers: - - * If `None`, after normalization, for each layer in *layers* each cell - has a total count equal to the median of the *counts_per_cell* before - normalization of the layer. - * If `'after'`, for each layer in *layers* each cell has - a total count equal to `target_sum`. - * If `'X'`, for each layer in *layers* each cell has a total count - equal to the median of total counts for observations (cells) of - `adata.X` before normalization. - + layer + Layer to normalize instead of `X`. If `None`, `X` is normalized. inplace Whether to update `adata` or return dictionary with normalized copies of `adata.X` and `adata.layers`. + copy + Whether to modify copied input object. Not compatible with inplace=False. Returns ------- @@ -127,9 +121,30 @@ def normalize_total( [ 0.5, 0.5, 0.5, 1. , 1. ], [ 0.5, 11. , 0.5, 1. , 1. ]], dtype=float32) """ + if copy: + if not inplace: + raise ValueError("`copy=True` cannot be used with `inplace=False`.") + adata = adata.copy() + if max_fraction < 0 or max_fraction > 1: raise ValueError('Choose max_fraction between 0 and 1.') + # Deprecated features + if layers is not None: + warn( + FutureWarning( + "The `layers` argument is deprecated. Instead, specify individual " + "layers to normalize with `layer`." + ) + ) + if layer_norm is not None: + warn( + FutureWarning( + "The `layer_norm` argument is deprecated. Specify the target size " + "factor directly with `target_sum`." + ) + ) + if layers == 'all': layers = adata.layers.keys() elif isinstance(layers, str): @@ -139,32 +154,47 @@ def normalize_total( view_to_actual(adata) + X = _get_obs_rep(adata, layer=layer) + gene_subset = None msg = 'normalizing counts per cell' if exclude_highly_expressed: - counts_per_cell = adata.X.sum(1) # original counts per cell + counts_per_cell = X.sum(1) # original counts per cell counts_per_cell = np.ravel(counts_per_cell) # at least one cell as more than max_fraction of counts per cell - gene_subset = (adata.X > counts_per_cell[:, None] * max_fraction).sum(0) + + gene_subset = (X > counts_per_cell[:, None] * max_fraction).sum(0) gene_subset = np.ravel(gene_subset) == 0 msg += ( ' The following highly-expressed genes are not considered during ' f'normalization factor computation:\n{adata.var_names[~gene_subset].tolist()}' ) + counts_per_cell = X[:, gene_subset].sum(1) + else: + counts_per_cell = X.sum(1) start = logg.info(msg) - - # counts per cell for subset, if max_fraction!=1 - X = adata.X if gene_subset is None else adata[:, gene_subset].X - counts_per_cell = X.sum(1) - # get rid of adata view - counts_per_cell = np.ravel(counts_per_cell).copy() + counts_per_cell = np.ravel(counts_per_cell) cell_subset = counts_per_cell > 0 if not np.all(cell_subset): - logg.warning('Some cells have total count of genes equal to zero') + warn(UserWarning('Some cells have zero counts')) + if inplace: + if key_added is not None: + adata.obs[key_added] = counts_per_cell + _set_obs_rep( + adata, _normalize_data(X, counts_per_cell, target_sum), layer=layer + ) + else: + # not recarray because need to support sparse + dat = dict( + X=_normalize_data(X, counts_per_cell, target_sum, copy=True), + norm_factor=counts_per_cell, + ) + + # Deprecated features if layer_norm == 'after': after = target_sum elif layer_norm == 'X': @@ -173,26 +203,13 @@ def normalize_total( after = None else: raise ValueError('layer_norm should be "after", "X" or None') - del cell_subset - if inplace: - if key_added is not None: - adata.obs[key_added] = counts_per_cell - adata.X = _normalize_data(adata.X, counts_per_cell, target_sum) - else: - # not recarray because need to support sparse - dat = dict( - X=_normalize_data(adata.X, counts_per_cell, target_sum, copy=True), - norm_factor=counts_per_cell, + for layer_to_norm in layers if layers is not None else (): + res = normalize_total( + adata, layer=layer_to_norm, target_sum=after, inplace=inplace ) - - for layer_name in layers or (): - layer = adata.layers[layer_name] - counts = np.ravel(layer.sum(1)) - if inplace: - adata.layers[layer_name] = _normalize_data(layer, counts, after) - else: - dat[layer_name] = _normalize_data(layer, counts, after, copy=True) + if not inplace: + dat[layer_to_norm] = res["X"] logg.info( ' finished ({time_passed})', @@ -203,4 +220,7 @@ def normalize_total( f'and added {key_added!r}, counts per cell before normalization (adata.obs)' ) - return dat if not inplace else None + if copy: + return adata + elif not inplace: + return dat diff --git a/scanpy/tests/helpers.py b/scanpy/tests/helpers.py index 35253bad69..61fc35e23e 100644 --- a/scanpy/tests/helpers.py +++ b/scanpy/tests/helpers.py @@ -2,68 +2,84 @@ This file contains helper functions for the scanpy test suite. """ +from itertools import permutations + import scanpy as sc import numpy as np from anndata.tests.helpers import asarray, assert_equal +# TODO: Report more context on the fields being compared on error +# TODO: Allow specifying paths to ignore on comparison + ########################### # Representation choice ########################### # These functions can be used to check that functions are correctly using arugments like `layers`, `obsm`, etc. -def check_rep_mutation(func, X, **kwargs): +def check_rep_mutation(func, X, *, fields=["layer", "obsm"], **kwargs): """Check that only the array meant to be modified is modified.""" - adata = sc.AnnData( - X=X.copy(), - layers={"layer": X.copy()}, - obsm={"obsm": X.copy()}, - dtype=X.dtype, - ) + adata = sc.AnnData(X=X.copy(), dtype=X.dtype) + for field in fields: + sc.get._set_obs_rep(adata, X, **{field: field}) + X_array = asarray(X) + adata_X = func(adata, copy=True, **kwargs) - adata_layer = func(adata, layer="layer", copy=True, **kwargs) - adata_obsm = func(adata, obsm="obsm", copy=True, **kwargs) + adatas_proc = { + field: func(adata, copy=True, **{field: field}, **kwargs) for field in fields + } - assert np.array_equal(asarray(adata_X.X), asarray(adata_layer.layers["layer"])) - assert np.array_equal(asarray(adata_X.X), asarray(adata_obsm.obsm["obsm"])) + # Modified fields + for field in fields: + result_array = asarray( + sc.get._get_obs_rep(adatas_proc[field], **{field: field}) + ) + np.testing.assert_array_equal(asarray(adata_X.X), result_array) - assert np.array_equal(asarray(adata_layer.X), asarray(adata_layer.obsm["obsm"])) - assert np.array_equal(asarray(adata_obsm.X), asarray(adata_obsm.layers["layer"])) - assert np.array_equal( - asarray(adata_X.layers["layer"]), asarray(adata_X.obsm["obsm"]) - ) + # Unmodified fields + for field in fields: + np.testing.assert_array_equal(X_array, asarray(adatas_proc[field].X)) + np.testing.assert_array_equal( + X_array, asarray(sc.get._get_obs_rep(adata_X, **{field: field})) + ) + for field_a, field_b in permutations(fields, 2): + result_array = asarray( + sc.get._get_obs_rep(adatas_proc[field_a], **{field_b: field_b}) + ) + np.testing.assert_array_equal(X_array, result_array) -def check_rep_results(func, X, **kwargs): +def check_rep_results(func, X, *, fields=["layer", "obsm"], **kwargs): """Checks that the results of a computation add values/ mutate the anndata object in a consistent way.""" # Gen data - adata_X = sc.AnnData( - X=X.copy(), - layers={"layer": np.zeros(shape=X.shape, dtype=X.dtype)}, - obsm={"obsm": np.zeros(shape=X.shape, dtype=X.dtype)}, - ) - adata_layer = sc.AnnData( - X=np.zeros(shape=X.shape, dtype=X.dtype), - layers={"layer": X.copy()}, - obsm={"obsm": np.zeros(shape=X.shape, dtype=X.dtype)}, - ) - adata_obsm = sc.AnnData( - X=np.zeros(shape=X.shape, dtype=X.dtype), - layers={"layer": np.zeros(shape=X.shape, dtype=X.dtype)}, - obsm={"obsm": X.copy()}, + empty_X = np.zeros(shape=X.shape, dtype=X.dtype) + adata = sc.AnnData( + X=empty_X.copy(), + layers={"layer": empty_X.copy()}, + obsm={"obsm": empty_X.copy()}, ) + adata_X = adata.copy() + adata_X.X = X.copy() + + adatas_proc = {} + for field in fields: + cur = adata.copy() + sc.get._set_obs_rep(cur, X.copy(), **{field: field}) + adatas_proc[field] = cur + # Apply function func(adata_X, **kwargs) - func(adata_layer, layer="layer", **kwargs) - func(adata_obsm, obsm="obsm", **kwargs) + for field in fields: + func(adatas_proc[field], **{field: field}, **kwargs) # Reset X - adata_X.X = np.zeros(shape=X.shape, dtype=X.dtype) - adata_layer.layers["layer"] = np.zeros(shape=X.shape, dtype=X.dtype) - adata_obsm.obsm["obsm"] = np.zeros(shape=X.shape, dtype=X.dtype) + adata_X.X = empty_X.copy() + for field in fields: + sc.get._set_obs_rep(adatas_proc[field], empty_X.copy(), **{field: field}) - # Check equality - assert_equal(adata_X, adata_layer) - assert_equal(adata_X, adata_obsm) + for field_a, field_b in permutations(fields, 2): + assert_equal(adatas_proc[field_a], adatas_proc[field_b]) + for field in fields: + assert_equal(adata_X, adatas_proc[field]) diff --git a/scanpy/tests/test_normalization.py b/scanpy/tests/test_normalization.py index 9b84699d9a..0f5dbb102d 100644 --- a/scanpy/tests/test_normalization.py +++ b/scanpy/tests/test_normalization.py @@ -2,9 +2,11 @@ import numpy as np from anndata import AnnData from scipy.sparse import csr_matrix +from scipy import sparse import scanpy as sc -from anndata.tests.helpers import assert_equal +from scanpy.tests.helpers import check_rep_mutation, check_rep_results +from anndata.tests.helpers import assert_equal, asarray X_total = [[1, 0], [3, 0], [5, 6]] X_frac = [[1, 0, 1], [3, 0, 1], [5, 6, 1]] @@ -24,12 +26,22 @@ def test_normalize_total(typ, dtype): assert np.allclose(np.ravel(adata.X[:, 1:3].sum(axis=1)), [1.0, 1.0, 1.0]) +@pytest.mark.parametrize('typ', [asarray, csr_matrix], ids=lambda x: x.__name__) +@pytest.mark.parametrize('dtype', ['float32', 'int64']) +def test_normalize_total_rep(typ, dtype): + # Test that layer kwarg works + X = typ(sparse.random(100, 50, format="csr", density=0.2, dtype=dtype)) + check_rep_mutation(sc.pp.normalize_total, X, fields=["layer"]) + check_rep_results(sc.pp.normalize_total, X, fields=["layer"]) + + @pytest.mark.parametrize('typ', [np.array, csr_matrix], ids=lambda x: x.__name__) @pytest.mark.parametrize('dtype', ['float32', 'int64']) def test_normalize_total_layers(typ, dtype): adata = AnnData(typ(X_total), dtype=dtype) adata.layers["layer"] = adata.X.copy() - sc.pp.normalize_total(adata, layers=["layer"]) + with pytest.warns(FutureWarning, match=r".*layers.*deprecated"): + sc.pp.normalize_total(adata, layers=["layer"]) assert np.allclose(adata.layers["layer"].sum(axis=1), [3.0, 3.0, 3.0]) From 1e814cb0ee48a76d5beaee437c3ef7dc4b85ed07 Mon Sep 17 00:00:00 2001 From: mjayasur Date: Tue, 9 Mar 2021 00:38:43 -0800 Subject: [PATCH 33/85] deprecate scvi (#1703) * deprecate scvi * Update .azure-pipelines.yml Co-authored-by: Isaac Virshup * remove :func: links to scvi in release notes * remove tildes in front of scvi in release notes * Update docs/release-notes/1.5.0.rst Co-authored-by: Michael Jayasuriya Co-authored-by: Isaac Virshup --- .azure-pipelines.yml | 2 +- .travis.yml | 4 +- docs/release-notes/1.5.0.rst | 3 +- docs/release-notes/1.7.0.rst | 2 +- pyproject.toml | 1 - scanpy/external/__init__.py | 1 - scanpy/external/pp/__init__.py | 1 - scanpy/tests/external/test_scvi.py | 61 ------------------------------ 8 files changed, 5 insertions(+), 70 deletions(-) delete mode 100644 scanpy/tests/external/test_scvi.py diff --git a/.azure-pipelines.yml b/.azure-pipelines.yml index 41bedee83a..d447367205 100644 --- a/.azure-pipelines.yml +++ b/.azure-pipelines.yml @@ -43,7 +43,7 @@ jobs: - script: | python -m pip install --upgrade pip pip install pytest-cov wheel - pip install .[dev,doc,test,louvain,leiden,magic,scvi,harmony,scrublet,scanorama] + pip install .[dev,doc,test,louvain,leiden,magic,harmony,scrublet,scanorama,skmisc] displayName: 'Install dependencies' - script: | diff --git a/.travis.yml b/.travis.yml index b8cd6c2ff2..f49c4ba726 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,7 +18,7 @@ matrix: - name: "anndata dev" python: "3.7" install: - - pip install .[dev,test,louvain,leiden,magic,scvi,harmony,skmisc,scrublet,scanorama] + - pip install .[dev,test,louvain,leiden,magic,harmony,skmisc,scrublet,scanorama] - pip install git+https://github.com/theislab/anndata python: - '3.6' @@ -29,7 +29,7 @@ cache: - directories: - data install: -- pip install .[dev,test,louvain,leiden,magic,scvi,harmony,skmisc,scrublet,scanorama] +- pip install .[dev,test,louvain,leiden,magic,harmony,skmisc,scrublet,scanorama] env: - MPLBACKEND=Agg script: diff --git a/docs/release-notes/1.5.0.rst b/docs/release-notes/1.5.0.rst index 12f31a3914..78fc339ff8 100644 --- a/docs/release-notes/1.5.0.rst +++ b/docs/release-notes/1.5.0.rst @@ -17,7 +17,7 @@ The `1.5.0` release adds a lot of new functionality, much of which takes advanta .. rubric:: External tools -- :func:`~scanpy.external.pp.scvi` for preprocessing with scVI :pr:`1085` :smaller:`G Xing` +- `scanpy.external.pp.scvi` for preprocessing with scVI :pr:`1085` :smaller:`G Xing` - Guide for using :ref:`Scanpy in R ` :pr:`1186` :smaller:`L Zappia` .. rubric:: Performance @@ -48,4 +48,3 @@ The `1.5.0` release adds a lot of new functionality, much of which takes advanta - :func:`~scanpy.tl.louvain` for Louvain `0.6` :pr:`1197` :smaller:`I Virshup` - :func:`~scanpy.pp.highly_variable_genes` which could lead to incorrect results when the `batch_key` argument was used :pr:`1180` :smaller:`G Eraslan` - :func:`~scanpy.tl.ingest` where an inconsistent number of neighbors was used :pr:`1111` :smaller:`S Rybakov` - diff --git a/docs/release-notes/1.7.0.rst b/docs/release-notes/1.7.0.rst index cc7dc8b6e6..19d257461a 100644 --- a/docs/release-notes/1.7.0.rst +++ b/docs/release-notes/1.7.0.rst @@ -27,7 +27,7 @@ - Updates for :func:`~scanpy.external.tl.palantir` and :func:`~scanpy.external.tl.palantir_results` :pr:`1245` :smaller:`A Mousa` - Fixes to :func:`~scanpy.external.tl.harmony_timeseries` docs :pr:`1248` :smaller:`A Mousa` - Support for `leiden` clustering by :func:`scanpy.external.tl.phenograph` :pr:`1080` :smaller:`A Mousa` -- Deprecate :func:`~scanpy.external.pp.scvi` :pr:`1554` :smaller:`G Xing` +- Deprecate `scanpy.external.pp.scvi` :pr:`1554` :smaller:`G Xing` - Updated default params of :func:`~scanpy.external.tl.sam` to work with larger data :pr:`1540` :smaller:`A Tarashansky` .. rubric:: Documentation diff --git a/pyproject.toml b/pyproject.toml index fe34eca22d..436e934584 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,7 +71,6 @@ requires = [ louvain = ['python-igraph', 'louvain>=0.6,!=0.6.2'] leiden = ['python-igraph', 'leidenalg'] bbknn = ['bbknn'] -scvi = ['scvi==0.6.7'] rapids = ['cudf>=0.9', 'cuml>=0.9', 'cugraph>=0.9'] magic = ['magic-impute>=2.0'] skmisc = ['scikit-misc>=0.1.3'] diff --git a/scanpy/external/__init__.py b/scanpy/external/__init__.py index 06f434234f..fab99d8f50 100644 --- a/scanpy/external/__init__.py +++ b/scanpy/external/__init__.py @@ -58,7 +58,6 @@ pp.dca pp.magic - pp.scvi Tools: TL diff --git a/scanpy/external/pp/__init__.py b/scanpy/external/pp/__init__.py index e7c76dc76d..50b07c002f 100644 --- a/scanpy/external/pp/__init__.py +++ b/scanpy/external/pp/__init__.py @@ -4,6 +4,5 @@ from ._harmony_integrate import harmony_integrate from ._magic import magic from ._scanorama_integrate import scanorama_integrate -from ._scvi import scvi from ._hashsolo import hashsolo from ._scrublet import scrublet, scrublet_simulate_doublets diff --git a/scanpy/tests/external/test_scvi.py b/scanpy/tests/external/test_scvi.py deleted file mode 100644 index a252c762dd..0000000000 --- a/scanpy/tests/external/test_scvi.py +++ /dev/null @@ -1,61 +0,0 @@ -import pytest -import sys -import numpy as numpy -import scanpy.external as sce -import numpy as np - -from anndata import AnnData - -pytest.importorskip("scvi", minversion=sce.pp._scvi.MIN_VERSION) - - -def test_scvi_linear(): - n_samples = 4 - n_genes = 7 - batch1 = np.random.randint(1, 5, size=(n_samples, n_genes)) - batch2 = np.random.randint(1, 5, size=(n_samples, n_genes)) - ad1 = AnnData(batch1) - ad2 = AnnData(batch2) - adata = ad1.concatenate(ad2, batch_categories=['test1', 'test2']) - n_latent = 30 - gene_subset = ['1', '4', '6'] - sce.pp.scvi( - adata, - use_cuda=False, - n_epochs=1, - n_latent=n_latent, - return_posterior=True, - batch_key='batch', - linear_decoder=True, - subset_genes=gene_subset, - ) - - assert adata.obsm['X_scvi'].shape == (n_samples * 2, n_latent) - assert adata.obsm['X_scvi_denoised'].shape == (n_samples * 2, len(gene_subset)) - assert adata.obsm['X_scvi_sample_rate'].shape == (n_samples * 2, len(gene_subset)) - assert adata.uns['ldvae_loadings'].shape == (len(gene_subset), n_latent) - assert len(adata.uns['ldvae_loadings'].index) == len(gene_subset) - assert set(adata.uns['ldvae_loadings'].index) == set(gene_subset) - - -def test_scvi(): - n_samples = 4 - n_genes = 7 - batch1 = np.random.randint(1, 5, size=(n_samples, n_genes)) - batch2 = np.random.randint(1, 5, size=(n_samples, n_genes)) - ad1 = AnnData(batch1) - ad2 = AnnData(batch2) - adata = ad1.concatenate(ad2, batch_categories=['test1', 'test2']) - n_latent = 30 - sce.pp.scvi( - adata, - use_cuda=False, - n_epochs=1, - n_latent=n_latent, - return_posterior=True, - batch_key='batch', - model_kwargs={'reconstruction_loss': 'nb'}, - ) - assert adata.obsm['X_scvi'].shape == (n_samples * 2, n_latent) - assert adata.obsm['X_scvi_denoised'].shape == adata.shape - assert adata.obsm['X_scvi_sample_rate'].shape == adata.shape From 056d183def711fefff88c3fc26fc85ea0feec23f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20M=2E=20Ascensi=C3=B3n?= <35657291+alexmascension@users.noreply.github.com> Date: Tue, 9 Mar 2021 09:39:54 +0100 Subject: [PATCH 34/85] updated ecosystem.rst to add triku (#1722) --- docs/ecosystem.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/ecosystem.rst b/docs/ecosystem.rst index b23a36026b..39c8de4aee 100644 --- a/docs/ecosystem.rst +++ b/docs/ecosystem.rst @@ -76,3 +76,8 @@ Adaptive immune receptor repertoire (AIRR) * `scirpy `__ :small:`Medical University of Innsbruck` | scirpy is a scanpy extension to expore single-cell T-cell receptor (TCR) and B-cell receptor (BCR) repertoires. + + +Feature selection +----------------- +* `triku 🦔 `__ :small:`Biodonostia Health Research Institute` From ade2975f522ac4164105da5c6e3ede2f244ac4f1 Mon Sep 17 00:00:00 2001 From: Isaac Virshup Date: Wed, 10 Mar 2021 14:59:08 +1100 Subject: [PATCH 35/85] Minor addition to contributing docs (#1726) --- docs/dev/getting-set-up.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/dev/getting-set-up.rst b/docs/dev/getting-set-up.rst index e67f21e4ec..7d1eb532d4 100644 --- a/docs/dev/getting-set-up.rst +++ b/docs/dev/getting-set-up.rst @@ -66,11 +66,14 @@ Creating a branch for your feature ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ All development should occur in branches dedicated to the particular work being done. +Additionally, unless you are a maintainer, all changes should be directed at the `master` branch. You can create a branch with: .. code:: shell - git checkout -b {your-branch-name} + git checkout master # Starting from the master branch + git pull # Syncing with the repo + git checkout -b {your-branch-name} # Making and changing to the new branch .. _open-a-pr: From 5f7f01ff9e7ccb447d3748f25c760641458df064 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6k=C3=A7en=20Eraslan?= Date: Thu, 11 Mar 2021 10:35:15 -0500 Subject: [PATCH 36/85] Preserve category order when groupby is a list (#1735) Preserve category order when groupby is a list --- scanpy/plotting/_anndata.py | 12 +++++++++++ .../master_dotplot_groupby_list_catorder.png | Bin 0 -> 14663 bytes scanpy/tests/test_plotting.py | 20 ++++++++++++++++++ 3 files changed, 32 insertions(+) create mode 100644 scanpy/tests/_images/master_dotplot_groupby_list_catorder.png diff --git a/scanpy/plotting/_anndata.py b/scanpy/plotting/_anndata.py index 6456f4857e..f92aebd19c 100755 --- a/scanpy/plotting/_anndata.py +++ b/scanpy/plotting/_anndata.py @@ -1866,6 +1866,18 @@ def _prepare_dataframe( categorical = obs_tidy[groupby].agg('_'.join, axis=1).astype('category') categorical.name = "_".join(groupby) + # preserve category order + from itertools import product + + order = { + "_".join(k): idx + for idx, k in enumerate( + product(*(obs_tidy[g].cat.categories for g in groupby)) + ) + } + categorical = categorical.cat.reorder_categories( + sorted(categorical.cat.categories, key=lambda x: order[x]) + ) obs_tidy = obs_tidy[var_names].set_index(categorical) categories = obs_tidy.index.categories diff --git a/scanpy/tests/_images/master_dotplot_groupby_list_catorder.png b/scanpy/tests/_images/master_dotplot_groupby_list_catorder.png new file mode 100644 index 0000000000000000000000000000000000000000..fd6c3453a032566018abd9f405b4278a5b0dc9d9 GIT binary patch literal 14663 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 TB-UV4Ba-B&&!VNmx_ Date: Sun, 14 Mar 2021 14:02:24 -0400 Subject: [PATCH 37/85] Asymmetrical diverging colormaps and vcenter (#1551) Add vcenter and norm arguments to plotting functions --- scanpy/plotting/_anndata.py | 45 ++++-- scanpy/plotting/_baseplot_class.py | 22 ++- scanpy/plotting/_docs.py | 42 +++-- scanpy/plotting/_dotplot.py | 42 +++-- scanpy/plotting/_matrixplot.py | 37 ++++- scanpy/plotting/_stacked_violin.py | 38 +++-- scanpy/plotting/_tools/__init__.py | 23 ++- scanpy/plotting/_tools/scatterplots.py | 57 ++++--- scanpy/plotting/_utils.py | 24 ++- ...lor.black_tup-legend.off-vbounds.norm].png | Bin 0 -> 23781 bytes ...black_tup-legend.off-vbounds.numbers].png} | Bin ...ck_tup-legend.off-vbounds.percentile].png} | Bin ....black_tup-legend.off-vbounds.vcenter].png | Bin 0 -> 24608 bytes ...black_tup-legend.on_data-vbounds.norm].png | Bin 0 -> 23781 bytes ...k_tup-legend.on_data-vbounds.numbers].png} | Bin ...up-legend.on_data-vbounds.percentile].png} | Bin ...ck_tup-legend.on_data-vbounds.vcenter].png | Bin 0 -> 24608 bytes ...lack_tup-legend.on_right-vbounds.norm].png | Bin 0 -> 23781 bytes ..._tup-legend.on_right-vbounds.numbers].png} | Bin ...p-legend.on_right-vbounds.percentile].png} | Bin ...k_tup-legend.on_right-vbounds.vcenter].png | Bin 0 -> 24608 bytes ...color.default-legend.off-vbounds.norm].png | Bin 0 -> 23194 bytes ...r.default-legend.off-vbounds.numbers].png} | Bin ...efault-legend.off-vbounds.percentile].png} | Bin ...or.default-legend.off-vbounds.vcenter].png | Bin 0 -> 23970 bytes ...r.default-legend.on_data-vbounds.norm].png | Bin 0 -> 23194 bytes ...fault-legend.on_data-vbounds.numbers].png} | Bin ...lt-legend.on_data-vbounds.percentile].png} | Bin ...efault-legend.on_data-vbounds.vcenter].png | Bin 0 -> 23970 bytes ....default-legend.on_right-vbounds.norm].png | Bin 0 -> 23194 bytes ...ault-legend.on_right-vbounds.numbers].png} | Bin ...t-legend.on_right-vbounds.percentile].png} | Bin ...fault-legend.on_right-vbounds.vcenter].png | Bin 0 -> 23970 bytes ...lor.black_tup-legend.off-vbounds.norm].png | Bin 0 -> 39676 bytes ...black_tup-legend.off-vbounds.numbers].png} | Bin ...ck_tup-legend.off-vbounds.percentile].png} | Bin ....black_tup-legend.off-vbounds.vcenter].png | Bin 0 -> 40153 bytes ...black_tup-legend.on_data-vbounds.norm].png | Bin 0 -> 39676 bytes ...k_tup-legend.on_data-vbounds.numbers].png} | Bin ...up-legend.on_data-vbounds.percentile].png} | Bin ...ck_tup-legend.on_data-vbounds.vcenter].png | Bin 0 -> 40153 bytes ...lack_tup-legend.on_right-vbounds.norm].png | Bin 0 -> 39676 bytes ..._tup-legend.on_right-vbounds.numbers].png} | Bin ...p-legend.on_right-vbounds.percentile].png} | Bin ...k_tup-legend.on_right-vbounds.vcenter].png | Bin 0 -> 40153 bytes ...color.default-legend.off-vbounds.norm].png | Bin 0 -> 35595 bytes ...r.default-legend.off-vbounds.numbers].png} | Bin ...efault-legend.off-vbounds.percentile].png} | Bin ...or.default-legend.off-vbounds.vcenter].png | Bin 0 -> 36119 bytes ...r.default-legend.on_data-vbounds.norm].png | Bin 0 -> 35595 bytes ...fault-legend.on_data-vbounds.numbers].png} | Bin ...lt-legend.on_data-vbounds.percentile].png} | Bin ...efault-legend.on_data-vbounds.vcenter].png | Bin 0 -> 36119 bytes ....default-legend.on_right-vbounds.norm].png | Bin 0 -> 35595 bytes ...ault-legend.on_right-vbounds.numbers].png} | Bin ...t-legend.on_right-vbounds.percentile].png} | Bin ...fault-legend.on_right-vbounds.vcenter].png | Bin 0 -> 36119 bytes .../master_embedding_outline_vmin_vmax.png | Bin 78561 -> 106635 bytes .../_images/master_multipanel_vcenter.png | Bin 0 -> 52657 bytes ...ed_genes_dotplot_logfoldchange_vcenter.png | Bin 0 -> 57075 bytes ...ranked_genes_heatmap_swap_axes_vcenter.png | Bin 0 -> 77301 bytes ...ked_genes_matrixplot_swap_axes_vcenter.png | Bin 0 -> 29380 bytes scanpy/tests/_images/master_umap_layer.png | Bin 26134 -> 26768 bytes scanpy/tests/test_embedding_plots.py | 26 +++- scanpy/tests/test_plotting.py | 146 +++++++++++++++++- 65 files changed, 414 insertions(+), 88 deletions(-) create mode 100644 scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[pca-na_color.black_tup-legend.off-vbounds.norm].png rename scanpy/tests/_images/embedding-missing-values/{test_missing_values_continuous[pca-na_color.black_tup-legend.off-vbound.numbers].png => test_missing_values_continuous[pca-na_color.black_tup-legend.off-vbounds.numbers].png} (100%) rename scanpy/tests/_images/embedding-missing-values/{test_missing_values_continuous[pca-na_color.black_tup-legend.off-vbound.percentile].png => test_missing_values_continuous[pca-na_color.black_tup-legend.off-vbounds.percentile].png} (100%) create mode 100644 scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[pca-na_color.black_tup-legend.off-vbounds.vcenter].png create mode 100644 scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[pca-na_color.black_tup-legend.on_data-vbounds.norm].png rename scanpy/tests/_images/embedding-missing-values/{test_missing_values_continuous[pca-na_color.black_tup-legend.on_data-vbound.numbers].png => test_missing_values_continuous[pca-na_color.black_tup-legend.on_data-vbounds.numbers].png} (100%) rename scanpy/tests/_images/embedding-missing-values/{test_missing_values_continuous[pca-na_color.black_tup-legend.on_data-vbound.percentile].png => test_missing_values_continuous[pca-na_color.black_tup-legend.on_data-vbounds.percentile].png} (100%) create mode 100644 scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[pca-na_color.black_tup-legend.on_data-vbounds.vcenter].png create mode 100644 scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[pca-na_color.black_tup-legend.on_right-vbounds.norm].png rename scanpy/tests/_images/embedding-missing-values/{test_missing_values_continuous[pca-na_color.black_tup-legend.on_right-vbound.numbers].png => test_missing_values_continuous[pca-na_color.black_tup-legend.on_right-vbounds.numbers].png} (100%) rename scanpy/tests/_images/embedding-missing-values/{test_missing_values_continuous[pca-na_color.black_tup-legend.on_right-vbound.percentile].png => test_missing_values_continuous[pca-na_color.black_tup-legend.on_right-vbounds.percentile].png} (100%) create mode 100644 scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[pca-na_color.black_tup-legend.on_right-vbounds.vcenter].png create mode 100644 scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[pca-na_color.default-legend.off-vbounds.norm].png rename scanpy/tests/_images/embedding-missing-values/{test_missing_values_continuous[pca-na_color.default-legend.off-vbound.numbers].png => test_missing_values_continuous[pca-na_color.default-legend.off-vbounds.numbers].png} (100%) rename scanpy/tests/_images/embedding-missing-values/{test_missing_values_continuous[pca-na_color.default-legend.off-vbound.percentile].png => test_missing_values_continuous[pca-na_color.default-legend.off-vbounds.percentile].png} (100%) create mode 100644 scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[pca-na_color.default-legend.off-vbounds.vcenter].png create mode 100644 scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[pca-na_color.default-legend.on_data-vbounds.norm].png rename scanpy/tests/_images/embedding-missing-values/{test_missing_values_continuous[pca-na_color.default-legend.on_data-vbound.numbers].png => test_missing_values_continuous[pca-na_color.default-legend.on_data-vbounds.numbers].png} (100%) rename scanpy/tests/_images/embedding-missing-values/{test_missing_values_continuous[pca-na_color.default-legend.on_data-vbound.percentile].png => test_missing_values_continuous[pca-na_color.default-legend.on_data-vbounds.percentile].png} (100%) create mode 100644 scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[pca-na_color.default-legend.on_data-vbounds.vcenter].png create mode 100644 scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[pca-na_color.default-legend.on_right-vbounds.norm].png rename scanpy/tests/_images/embedding-missing-values/{test_missing_values_continuous[pca-na_color.default-legend.on_right-vbound.numbers].png => test_missing_values_continuous[pca-na_color.default-legend.on_right-vbounds.numbers].png} (100%) rename scanpy/tests/_images/embedding-missing-values/{test_missing_values_continuous[pca-na_color.default-legend.on_right-vbound.percentile].png => test_missing_values_continuous[pca-na_color.default-legend.on_right-vbounds.percentile].png} (100%) create mode 100644 scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[pca-na_color.default-legend.on_right-vbounds.vcenter].png create mode 100644 scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.black_tup-legend.off-vbounds.norm].png rename scanpy/tests/_images/embedding-missing-values/{test_missing_values_continuous[spatial-na_color.black_tup-legend.off-vbound.numbers].png => test_missing_values_continuous[spatial-na_color.black_tup-legend.off-vbounds.numbers].png} (100%) rename scanpy/tests/_images/embedding-missing-values/{test_missing_values_continuous[spatial-na_color.black_tup-legend.off-vbound.percentile].png => test_missing_values_continuous[spatial-na_color.black_tup-legend.off-vbounds.percentile].png} (100%) create mode 100644 scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.black_tup-legend.off-vbounds.vcenter].png create mode 100644 scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.black_tup-legend.on_data-vbounds.norm].png rename scanpy/tests/_images/embedding-missing-values/{test_missing_values_continuous[spatial-na_color.black_tup-legend.on_data-vbound.numbers].png => test_missing_values_continuous[spatial-na_color.black_tup-legend.on_data-vbounds.numbers].png} (100%) rename scanpy/tests/_images/embedding-missing-values/{test_missing_values_continuous[spatial-na_color.black_tup-legend.on_data-vbound.percentile].png => test_missing_values_continuous[spatial-na_color.black_tup-legend.on_data-vbounds.percentile].png} (100%) create mode 100644 scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.black_tup-legend.on_data-vbounds.vcenter].png create mode 100644 scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.black_tup-legend.on_right-vbounds.norm].png rename scanpy/tests/_images/embedding-missing-values/{test_missing_values_continuous[spatial-na_color.black_tup-legend.on_right-vbound.numbers].png => test_missing_values_continuous[spatial-na_color.black_tup-legend.on_right-vbounds.numbers].png} (100%) rename scanpy/tests/_images/embedding-missing-values/{test_missing_values_continuous[spatial-na_color.black_tup-legend.on_right-vbound.percentile].png => test_missing_values_continuous[spatial-na_color.black_tup-legend.on_right-vbounds.percentile].png} (100%) create mode 100644 scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.black_tup-legend.on_right-vbounds.vcenter].png create mode 100644 scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.default-legend.off-vbounds.norm].png rename scanpy/tests/_images/embedding-missing-values/{test_missing_values_continuous[spatial-na_color.default-legend.off-vbound.numbers].png => test_missing_values_continuous[spatial-na_color.default-legend.off-vbounds.numbers].png} (100%) rename scanpy/tests/_images/embedding-missing-values/{test_missing_values_continuous[spatial-na_color.default-legend.off-vbound.percentile].png => test_missing_values_continuous[spatial-na_color.default-legend.off-vbounds.percentile].png} (100%) create mode 100644 scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.default-legend.off-vbounds.vcenter].png create mode 100644 scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.default-legend.on_data-vbounds.norm].png rename scanpy/tests/_images/embedding-missing-values/{test_missing_values_continuous[spatial-na_color.default-legend.on_data-vbound.numbers].png => test_missing_values_continuous[spatial-na_color.default-legend.on_data-vbounds.numbers].png} (100%) rename scanpy/tests/_images/embedding-missing-values/{test_missing_values_continuous[spatial-na_color.default-legend.on_data-vbound.percentile].png => test_missing_values_continuous[spatial-na_color.default-legend.on_data-vbounds.percentile].png} (100%) create mode 100644 scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.default-legend.on_data-vbounds.vcenter].png create mode 100644 scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.default-legend.on_right-vbounds.norm].png rename scanpy/tests/_images/embedding-missing-values/{test_missing_values_continuous[spatial-na_color.default-legend.on_right-vbound.numbers].png => test_missing_values_continuous[spatial-na_color.default-legend.on_right-vbounds.numbers].png} (100%) rename scanpy/tests/_images/embedding-missing-values/{test_missing_values_continuous[spatial-na_color.default-legend.on_right-vbound.percentile].png => test_missing_values_continuous[spatial-na_color.default-legend.on_right-vbounds.percentile].png} (100%) create mode 100644 scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.default-legend.on_right-vbounds.vcenter].png create mode 100644 scanpy/tests/_images/master_multipanel_vcenter.png create mode 100644 scanpy/tests/_images/master_ranked_genes_dotplot_logfoldchange_vcenter.png create mode 100644 scanpy/tests/_images/master_ranked_genes_heatmap_swap_axes_vcenter.png create mode 100644 scanpy/tests/_images/master_ranked_genes_matrixplot_swap_axes_vcenter.png diff --git a/scanpy/plotting/_anndata.py b/scanpy/plotting/_anndata.py index f92aebd19c..6c319682ce 100755 --- a/scanpy/plotting/_anndata.py +++ b/scanpy/plotting/_anndata.py @@ -18,7 +18,7 @@ from matplotlib import rcParams from matplotlib import gridspec from matplotlib import patheffects -from matplotlib.colors import is_color_like, Colormap, ListedColormap +from matplotlib.colors import is_color_like, Colormap, ListedColormap, Normalize from .. import get from .. import logging as logg @@ -26,9 +26,14 @@ from .._utils import sanitize_anndata, _doc_params, _check_use_raw from .._compat import Literal from . import _utils -from ._utils import scatter_base, scatter_group, setup_axes +from ._utils import scatter_base, scatter_group, setup_axes, check_colornorm from ._utils import ColorLike, _FontWeight, _FontSize -from ._docs import doc_scatter_basic, doc_show_save_ax, doc_common_plot_args +from ._docs import ( + doc_scatter_basic, + doc_show_save_ax, + doc_common_plot_args, + doc_vboundnorm, +) VALID_LEGENDLOCS = { 'none', @@ -890,7 +895,11 @@ def clustermap( return g -@_doc_params(show_save_ax=doc_show_save_ax, common_plot_args=doc_common_plot_args) +@_doc_params( + vminmax=doc_vboundnorm, + show_save_ax=doc_show_save_ax, + common_plot_args=doc_common_plot_args, +) def heatmap( adata: AnnData, var_names: Union[_VarNames, Mapping[str, _VarNames]], @@ -910,6 +919,10 @@ def heatmap( show: Optional[bool] = None, save: Union[str, bool, None] = None, figsize: Optional[Tuple[float, float]] = None, + vmin: Optional[float] = None, + vmax: Optional[float] = None, + vcenter: Optional[float] = None, + norm: Optional[Normalize] = None, **kwds, ): """\ @@ -933,6 +946,7 @@ def heatmap( show_gene_labels By default gene labels are shown when there are 50 or less genes. Otherwise the labels are removed. {show_save_ax} + {vminmax} **kwds Are passed to :func:`matplotlib.pyplot.imshow`. @@ -1057,6 +1071,7 @@ def heatmap( obs_tidy = obs_tidy.sort_index() colorbar_width = 0.2 + norm = check_colornorm(vmin, vmax, vcenter, norm) if not swap_axes: # define a layout of 2 rows x 4 columns @@ -1106,7 +1121,7 @@ def heatmap( heatmap_ax = fig.add_subplot(axs[1, 1]) kwds.setdefault('interpolation', 'nearest') - im = heatmap_ax.imshow(obs_tidy.values, aspect='auto', **kwds) + im = heatmap_ax.imshow(obs_tidy.values, aspect='auto', norm=norm, **kwds) heatmap_ax.set_ylim(obs_tidy.shape[0] - 0.5, -0.5) heatmap_ax.set_xlim(-0.5, obs_tidy.shape[1] - 0.5) @@ -1211,7 +1226,7 @@ def heatmap( heatmap_ax = fig.add_subplot(axs[1, 0]) kwds.setdefault('interpolation', 'nearest') - im = heatmap_ax.imshow(obs_tidy.T.values, aspect='auto', **kwds) + im = heatmap_ax.imshow(obs_tidy.T.values, aspect='auto', norm=norm, **kwds) heatmap_ax.set_xlim(0 - 0.5, obs_tidy.shape[0] - 0.5) heatmap_ax.set_ylim(obs_tidy.shape[1] - 0.5, -0.5) heatmap_ax.tick_params(axis='x', bottom=False, labelbottom=False) @@ -1603,7 +1618,7 @@ def dendrogram( return ax -@_doc_params(show_save_ax=doc_show_save_ax) +@_doc_params(show_save_ax=doc_show_save_ax, vminmax=doc_vboundnorm) def correlation_matrix( adata: AnnData, groupby: str, @@ -1613,6 +1628,10 @@ def correlation_matrix( show: Optional[bool] = None, save: Union[str, bool, None] = None, ax: Optional[Axes] = None, + vmin: Optional[float] = None, + vmax: Optional[float] = None, + vcenter: Optional[float] = None, + norm: Optional[Normalize] = None, **kwds, ) -> Union[Axes, List[Axes]]: """\ @@ -1635,10 +1654,11 @@ def correlation_matrix( By default a figure size that aims to produce a squared correlation matrix plot is used. Format is (width, height) {show_save_ax} + {vminmax} **kwds Only if `show_correlation` is True: Are passed to :func:`matplotlib.pyplot.pcolormesh` when plotting the - correlation heatmap. Useful values to pas are `vmax`, `vmin` and `cmap`. + correlation heatmap. `cmap` can be used to change the color palette. Returns ------- @@ -1718,14 +1738,15 @@ def correlation_matrix( else: kwds['edgecolors'] = 'black' kwds['linewidth'] = 0.01 - if 'vmax' not in kwds and 'vmin' not in kwds: - kwds['vmax'] = 1 - kwds['vmin'] = -1 + if vmax is None and vmin is None and norm is None: + vmax = 1 + vmin = -1 + norm = check_colornorm(vmin, vmax, vcenter, norm) if 'cmap' not in kwds: # by default use a divergent color map kwds['cmap'] = 'bwr' - img_mat = corr_matrix_ax.pcolormesh(corr_matrix, **kwds) + img_mat = corr_matrix_ax.pcolormesh(corr_matrix, norm=norm, **kwds) corr_matrix_ax.set_xlim(0, num_rows) corr_matrix_ax.set_ylim(0, num_rows) diff --git a/scanpy/plotting/_baseplot_class.py b/scanpy/plotting/_baseplot_class.py index f8edc819dd..e28a535fed 100644 --- a/scanpy/plotting/_baseplot_class.py +++ b/scanpy/plotting/_baseplot_class.py @@ -1,6 +1,7 @@ """BasePlot for dotplot, matrixplot and stacked_violin """ import collections.abc as cabc +from collections import namedtuple from typing import Optional, Union, Mapping # Special from typing import Sequence, Iterable # ABCs from typing import Tuple # Classes @@ -10,11 +11,12 @@ from matplotlib.axes import Axes from matplotlib import pyplot as pl from matplotlib import gridspec +from matplotlib.colors import Normalize from warnings import warn from .. import logging as logg from .._compat import Literal -from ._utils import make_grid_spec +from ._utils import make_grid_spec, check_colornorm from ._utils import ColorLike, _AxesSubplot from ._anndata import _plot_dendrogram, _get_dendrogram_key, _prepare_dataframe @@ -86,6 +88,10 @@ def __init__( var_group_rotation: Optional[float] = None, layer: Optional[str] = None, ax: Optional[_AxesSubplot] = None, + vmin: Optional[float] = None, + vmax: Optional[float] = None, + vcenter: Optional[float] = None, + norm: Optional[Normalize] = None, **kwds, ): self.var_names = var_names @@ -136,6 +142,9 @@ def __init__( self.log = log self.kwds = kwds + VBoundNorm = namedtuple('VBoundNorm', ['vmin', 'vmax', 'vcenter', 'norm']) + self.vboundnorm = VBoundNorm(vmin=vmin, vmax=vmax, vcenter=vcenter, norm=norm) + # set default values for legend self.color_legend_title = self.DEFAULT_COLOR_LEGEND_TITLE self.legends_width = self.DEFAULT_LEGENDS_WIDTH @@ -529,8 +538,6 @@ def _plot_legend(self, legend_ax, return_ax_dict, normalize): return_ax_dict['color_legend_ax'] = color_legend_ax def _mainplot(self, ax): - import matplotlib.colors - y_labels = self.categories x_labels = self.var_names @@ -563,12 +570,13 @@ def _mainplot(self, ax): ax.set_ylim(len(y_labels), 0) ax.set_xlim(0, len(x_labels)) - normalize = matplotlib.colors.Normalize( - vmin=self.kwds.get('vmin'), vmax=self.kwds.get('vmax') + return check_colornorm( + self.vboundnorm.vmin, + self.vboundnorm.vmax, + self.vboundnorm.vcenter, + self.vboundnorm.norm, ) - return normalize - def make_figure(self): """ Renders the image but does not call :func:`matplotlib.pyplot.show`. Useful diff --git a/scanpy/plotting/_docs.py b/scanpy/plotting/_docs.py index a53bde71a9..9ce750d8f9 100644 --- a/scanpy/plotting/_docs.py +++ b/scanpy/plotting/_docs.py @@ -101,19 +101,37 @@ e.g. `['title1', 'title2', ...]`. """ -doc_vminmax = """\ +doc_vbound_percentile = """\ vmin - Minimum value to plot. Values smaller than vmin are plotted with the same color as vmin. - vmin can be a number, a string, a function or `None`. If vmin is a string and has the format `pN`, - this is interpreted as a vmin=percentile(N). For example vmin='p1.5' is interpreted as - the 1.5 percentile. If vmin is function, then vmin is interpreted as the return value - of the function over the list of values to plot. For example to set vmin tp the mean of - the values to plot, `def my_vmin(values): return np.mean(values)` and then - set `vmin=my_vmin`. If vmin is None (default) an automatic minimum value is used - as defined by matplotlib `scatter` function. When making multiple plots, vmin can - be a list of values, one for each plot. For example `vmin=[0.1, 'p1', None, my_vmin]` + The value representing the lower limit of the color scale. Values smaller than vmin are plotted + with the same color as vmin. vmin can be a number, a string, a function or `None`. If + vmin is a string and has the format `pN`, this is interpreted as a vmin=percentile(N). + For example vmin='p1.5' is interpreted as the 1.5 percentile. If vmin is function, then + vmin is interpreted as the return value of the function over the list of values to plot. + For example to set vmin tp the mean of the values to plot, `def my_vmin(values): return + np.mean(values)` and then set `vmin=my_vmin`. If vmin is None (default) an automatic + minimum value is used as defined by matplotlib `scatter` function. When making multiple + plots, vmin can be a list of values, one for each plot. For example `vmin=[0.1, 'p1', None, my_vmin]` vmax - Maximum value to plot. The format is the same as for `vmin`\ + The value representing the upper limit of the color scale. The format is the same as for `vmin`. +vcenter + The value representing the center of the color scale. Useful for diverging colormaps. + The format is the same as for `vmin`. + Example: sc.pl.umap(adata, color='TREM2', vcenter='p50', cmap='RdBu_r')\ +""" + +doc_vboundnorm = """\ +vmin + The value representing the lower limit of the color scale. Values smaller than vmin are plotted + with the same color as vmin. +vmax + The value representing the upper limit of the color scale. Values larger than vmax are plotted + with the same color as vmax. +vcenter + The value representing the center of the color scale. Useful for diverging colormaps. +norm + Custom color normalization object from matplotlib. See + `https://matplotlib.org/stable/tutorials/colors/colormapnorms.html` for details.\ """ doc_outline = """\ @@ -144,7 +162,7 @@ # Docs for pl.pca, pl.tsne, … (everything in _tools.scatterplots) doc_scatter_embedding = f"""\ {doc_scatter_basic} -{doc_vminmax} +{doc_vbound_percentile} {doc_outline} {doc_panels} kwargs diff --git a/scanpy/plotting/_dotplot.py b/scanpy/plotting/_dotplot.py index c63f0b46e5..de1ef06c41 100644 --- a/scanpy/plotting/_dotplot.py +++ b/scanpy/plotting/_dotplot.py @@ -7,16 +7,17 @@ from anndata import AnnData from matplotlib.axes import Axes from matplotlib import pyplot as pl +from matplotlib.colors import Normalize from .. import logging as logg from .._utils import _doc_params from .._compat import Literal -from ._utils import make_grid_spec, fix_kwds +from ._utils import make_grid_spec, fix_kwds, check_colornorm from ._utils import ColorLike, _AxesSubplot from ._utils import savefig_or_show from .._settings import settings -from ._docs import doc_common_plot_args, doc_show_save_ax +from ._docs import doc_common_plot_args, doc_show_save_ax, doc_vboundnorm from ._baseplot_class import BasePlot, doc_common_groupby_plot_args, _VarNames @@ -122,6 +123,10 @@ def __init__( dot_color_df: Optional[pd.DataFrame] = None, dot_size_df: Optional[pd.DataFrame] = None, ax: Optional[_AxesSubplot] = None, + vmin: Optional[float] = None, + vmax: Optional[float] = None, + vcenter: Optional[float] = None, + norm: Optional[Normalize] = None, **kwds, ): BasePlot.__init__( @@ -141,6 +146,10 @@ def __init__( var_group_rotation=var_group_rotation, layer=layer, ax=ax, + vmin=vmin, + vmax=vmax, + vcenter=vcenter, + norm=norm, **kwds, ) @@ -536,6 +545,10 @@ def _mainplot(self, ax): grid=self.grid, x_padding=self.plot_x_padding, y_padding=self.plot_y_padding, + vmin=self.vboundnorm.vmin, + vmax=self.vboundnorm.vmax, + vcenter=self.vboundnorm.vcenter, + norm=self.vboundnorm.norm, **self.kwds, ) @@ -561,6 +574,10 @@ def _dotplot( grid: Optional[bool] = False, x_padding: Optional[float] = 0.8, y_padding: Optional[float] = 1.0, + vmin: Optional[float] = None, + vmax: Optional[float] = None, + vcenter: Optional[float] = None, + norm: Optional[Normalize] = None, **kwds, ): """\ @@ -682,12 +699,7 @@ def _dotplot( size = frac ** size_exponent # rescale size to match smallest_dot and largest_dot size = size * (largest_dot - smallest_dot) + smallest_dot - - import matplotlib.colors - - normalize = matplotlib.colors.Normalize( - vmin=kwds.get('vmin'), vmax=kwds.get('vmax') - ) + normalize = check_colornorm(vmin, vmax, vcenter, norm) if color_on == 'square': if edge_color is None: @@ -713,10 +725,10 @@ def _dotplot( kwds, s=size, cmap=cmap, - norm=None, linewidth=edge_lw, facecolor='none', edgecolor=edge_color, + norm=normalize, ) dot_ax.scatter(x, y, **kwds) else: @@ -729,9 +741,9 @@ def _dotplot( s=size, cmap=cmap, color=color, - norm=None, linewidth=edge_lw, edgecolor=edge_color, + norm=normalize, ) dot_ax.scatter(x, y, **kwds) @@ -782,6 +794,7 @@ def _dotplot( show_save_ax=doc_show_save_ax, common_plot_args=doc_common_plot_args, groupby_plots_args=doc_common_groupby_plot_args, + vminmax=doc_vboundnorm, ) def dotplot( adata: AnnData, @@ -813,6 +826,10 @@ def dotplot( save: Union[str, bool, None] = None, ax: Optional[_AxesSubplot] = None, return_fig: Optional[bool] = False, + vmin: Optional[float] = None, + vmax: Optional[float] = None, + vcenter: Optional[float] = None, + norm: Optional[Normalize] = None, **kwds, ) -> Union[DotPlot, dict, None]: """\ @@ -861,6 +878,7 @@ def dotplot( If none, the smallest dot has size 0. All expression levels with `dot_min` are plotted with this size. {show_save_ax} + {vminmax} kwds Are passed to :func:`matplotlib.pyplot.scatter`. @@ -940,6 +958,10 @@ def dotplot( layer=layer, dot_color_df=dot_color_df, ax=ax, + vmin=vmin, + vmax=vmax, + vcenter=vcenter, + norm=norm, **kwds, ) diff --git a/scanpy/plotting/_matrixplot.py b/scanpy/plotting/_matrixplot.py index d95678ba06..99a23b2300 100644 --- a/scanpy/plotting/_matrixplot.py +++ b/scanpy/plotting/_matrixplot.py @@ -7,15 +7,20 @@ from anndata import AnnData from matplotlib import pyplot as pl from matplotlib import rcParams +from matplotlib.colors import Normalize from .. import logging as logg from .._utils import _doc_params from .._compat import Literal -from ._utils import fix_kwds +from ._utils import fix_kwds, check_colornorm from ._utils import ColorLike, _AxesSubplot from ._utils import savefig_or_show from .._settings import settings -from ._docs import doc_common_plot_args, doc_show_save_ax +from ._docs import ( + doc_common_plot_args, + doc_show_save_ax, + doc_vboundnorm, +) from ._baseplot_class import BasePlot, doc_common_groupby_plot_args, _VarNames @@ -97,6 +102,10 @@ def __init__( standard_scale: Literal['var', 'group'] = None, ax: Optional[_AxesSubplot] = None, values_df: Optional[pd.DataFrame] = None, + vmin: Optional[float] = None, + vmax: Optional[float] = None, + vcenter: Optional[float] = None, + norm: Optional[Normalize] = None, **kwds, ): BasePlot.__init__( @@ -116,6 +125,10 @@ def __init__( var_group_rotation=var_group_rotation, layer=layer, ax=ax, + vmin=vmin, + vmax=vmax, + vcenter=vcenter, + norm=norm, **kwds, ) @@ -202,11 +215,11 @@ def _mainplot(self, ax): cmap = pl.get_cmap(self.kwds.get('cmap', self.cmap)) if 'cmap' in self.kwds: del self.kwds['cmap'] - - import matplotlib.colors - - normalize = matplotlib.colors.Normalize( - vmin=self.kwds.get('vmin'), vmax=self.kwds.get('vmax') + normalize = check_colornorm( + self.vboundnorm.vmin, + self.vboundnorm.vmax, + self.vboundnorm.vcenter, + self.vboundnorm.norm, ) for axis in ['top', 'bottom', 'left', 'right']: @@ -248,6 +261,7 @@ def _mainplot(self, ax): show_save_ax=doc_show_save_ax, common_plot_args=doc_common_plot_args, groupby_plots_args=doc_common_groupby_plot_args, + vminmax=doc_vboundnorm, ) def matrixplot( adata: AnnData, @@ -273,6 +287,10 @@ def matrixplot( save: Union[str, bool, None] = None, ax: Optional[_AxesSubplot] = None, return_fig: Optional[bool] = False, + vmin: Optional[float] = None, + vmax: Optional[float] = None, + vcenter: Optional[float] = None, + norm: Optional[Normalize] = None, **kwds, ) -> Union[MatrixPlot, dict, None]: """\ @@ -287,6 +305,7 @@ def matrixplot( {common_plot_args} {groupby_plots_args} {show_save_ax} + {vminmax} kwds Are passed to :func:`matplotlib.pyplot.pcolor`. @@ -342,6 +361,10 @@ def matrixplot( layer=layer, values_df=values_df, ax=ax, + vmin=vmin, + vmax=vmax, + vcenter=vcenter, + norm=norm, **kwds, ) diff --git a/scanpy/plotting/_stacked_violin.py b/scanpy/plotting/_stacked_violin.py index 16abbaeeb9..347c8cc569 100644 --- a/scanpy/plotting/_stacked_violin.py +++ b/scanpy/plotting/_stacked_violin.py @@ -6,16 +6,16 @@ import pandas as pd from anndata import AnnData from matplotlib import pyplot as pl -from matplotlib.colors import is_color_like +from matplotlib.colors import is_color_like, Normalize from .. import logging as logg from .._utils import _doc_params from .._compat import Literal -from ._utils import make_grid_spec +from ._utils import make_grid_spec, check_colornorm from ._utils import _AxesSubplot from ._utils import savefig_or_show from .._settings import settings -from ._docs import doc_common_plot_args, doc_show_save_ax +from ._docs import doc_common_plot_args, doc_show_save_ax, doc_vboundnorm from ._baseplot_class import BasePlot, doc_common_groupby_plot_args, _VarNames @@ -142,6 +142,10 @@ def __init__( layer: Optional[str] = None, standard_scale: Literal['var', 'group'] = None, ax: Optional[_AxesSubplot] = None, + vmin: Optional[float] = None, + vmax: Optional[float] = None, + vcenter: Optional[float] = None, + norm: Optional[Normalize] = None, **kwds, ): BasePlot.__init__( @@ -161,6 +165,10 @@ def __init__( var_group_rotation=var_group_rotation, layer=layer, ax=ax, + vmin=vmin, + vmax=vmax, + vcenter=vcenter, + norm=norm, **kwds, ) @@ -317,15 +325,17 @@ def _mainplot(self, ax): _color_df = _matrix.groupby(level=0).median() if self.are_axes_swapped: _color_df = _color_df.T - import matplotlib.colors - norm = matplotlib.colors.Normalize( - vmin=self.kwds.get('vmin'), vmax=self.kwds.get('vmax') - ) cmap = pl.get_cmap(self.kwds.get('cmap', self.cmap)) if 'cmap' in self.kwds: del self.kwds['cmap'] - colormap_array = cmap(norm(_color_df.values)) + normalize = check_colornorm( + self.vboundnorm.vmin, + self.vboundnorm.vmax, + self.vboundnorm.vcenter, + self.vboundnorm.norm, + ) + colormap_array = cmap(normalize(_color_df.values)) x_spacer_size = self.plot_x_padding y_spacer_size = self.plot_y_padding self._make_rows_of_violinplots( @@ -360,7 +370,7 @@ def _mainplot(self, ax): ax.tick_params(axis='both', labelsize='small') ax.grid(False) - return norm + return normalize def _make_rows_of_violinplots( self, ax, _matrix, colormap_array, _color_df, x_spacer_size, y_spacer_size @@ -540,6 +550,7 @@ def _setup_violin_axes_ticks(self, row_ax, num_cols): show_save_ax=doc_show_save_ax, common_plot_args=doc_common_plot_args, groupby_plots_args=doc_common_groupby_plot_args, + vminmax=doc_vboundnorm, ) def stacked_violin( adata: AnnData, @@ -571,6 +582,10 @@ def stacked_violin( row_palette: Optional[str] = StackedViolin.DEFAULT_ROW_PALETTE, cmap: Optional[str] = StackedViolin.DEFAULT_COLORMAP, ax: Optional[_AxesSubplot] = None, + vmin: Optional[float] = None, + vmax: Optional[float] = None, + vcenter: Optional[float] = None, + norm: Optional[Normalize] = None, **kwds, ) -> Union[StackedViolin, dict, None]: """\ @@ -618,6 +633,7 @@ def stacked_violin( Alternatively, a single color name or hex value can be passed, e.g. `'red'` or `'#cc33ff'`. {show_save_ax} + {vminmax} kwds Are passed to :func:`~seaborn.violinplot`. @@ -676,6 +692,10 @@ def stacked_violin( var_group_rotation=var_group_rotation, layer=layer, ax=ax, + vmin=vmin, + vmax=vmax, + vcenter=vcenter, + norm=norm, **kwds, ) diff --git a/scanpy/plotting/_tools/__init__.py b/scanpy/plotting/_tools/__init__.py index 7f7b76e04f..42e4a7da88 100644 --- a/scanpy/plotting/_tools/__init__.py +++ b/scanpy/plotting/_tools/__init__.py @@ -5,6 +5,7 @@ from cycler import Cycler from matplotlib.axes import Axes from matplotlib.figure import Figure +from matplotlib.colors import Normalize from scipy.sparse import issparse from matplotlib import pyplot as pl from matplotlib import rcParams, cm, colors @@ -18,7 +19,12 @@ from .._anndata import ranking from .._utils import timeseries, timeseries_subplot, timeseries_as_heatmap from ..._settings import settings -from .._docs import doc_scatter_embedding, doc_show_save_ax, doc_vminmax, doc_panels +from .._docs import ( + doc_scatter_embedding, + doc_show_save_ax, + doc_vbound_percentile, + doc_panels, +) from ...get import rank_genes_groups_df from .scatterplots import pca, embedding, _panel_grid from matplotlib.colors import Colormap @@ -1034,7 +1040,9 @@ def sim( savefig_or_show('sim_shuffled', save=save, show=show) -@_doc_params(vminmax=doc_vminmax, panels=doc_panels, show_save_ax=doc_show_save_ax) +@_doc_params( + vminmax=doc_vbound_percentile, panels=doc_panels, show_save_ax=doc_show_save_ax +) def embedding_density( adata: AnnData, # on purpose, there is no asterisk here (for backward compat) @@ -1047,6 +1055,8 @@ def embedding_density( fg_dotsize: Optional[int] = 180, vmax: Optional[int] = 1, vmin: Optional[int] = 0, + vcenter: Optional[int] = None, + norm: Optional[Normalize] = None, ncols: Optional[int] = 4, hspace: Optional[float] = 0.25, wspace: Optional[None] = None, @@ -1201,7 +1211,6 @@ def embedding_density( if isinstance(color_map, str): color_map = copy(cm.get_cmap(color_map)) - norm = colors.Normalize(vmin=vmin, vmax=vmax) color_map.set_over('black') color_map.set_under('lightgray') # a name to store the density values is needed. To avoid @@ -1252,8 +1261,11 @@ def embedding_density( components=components, color=density_col_name, color_map=color_map, - norm=norm, size=dot_sizes, + vmax=vmax, + vmin=vmin, + vcenter=vcenter, + norm=norm, save=False, title=_title, ax=ax, @@ -1280,10 +1292,11 @@ def embedding_density( components=components, color=density_col_name, color_map=color_map, - norm=norm, size=dot_sizes, vmax=vmax, vmin=vmin, + vcenter=vcenter, + norm=norm, save=False, show=False, title=title, diff --git a/scanpy/plotting/_tools/scatterplots.py b/scanpy/plotting/_tools/scatterplots.py index 1c17f4566d..154ffb7a91 100644 --- a/scanpy/plotting/_tools/scatterplots.py +++ b/scanpy/plotting/_tools/scatterplots.py @@ -1,6 +1,6 @@ import collections.abc as cabc from copy import copy -from typing import Union, Optional, Sequence, Any, Mapping, List, Tuple, Callable +from typing import Union, Optional, Sequence, Any, Mapping, List, Tuple import numpy as np import pandas as pd @@ -13,7 +13,7 @@ from matplotlib.cm import get_cmap from matplotlib import rcParams from matplotlib import patheffects -from matplotlib.colors import Colormap +from matplotlib.colors import Colormap, Normalize from functools import partial from .. import _utils @@ -21,9 +21,11 @@ _IGraphLayout, _FontWeight, _FontSize, - circles, ColorLike, + VBound, + circles, check_projection, + check_colornorm, ) from .._docs import ( doc_adata_color_etc, @@ -37,8 +39,6 @@ from ..._utils import sanitize_anndata, _doc_params, Empty, _empty from ..._compat import Literal -VMinMax = Union[str, float, Callable[[Sequence[float]], float]] - @_doc_params( adata_color_etc=doc_adata_color_etc, @@ -76,8 +76,10 @@ def embedding( legend_fontweight: Union[int, _FontWeight] = 'bold', legend_loc: str = 'right margin', legend_fontoutline: Optional[int] = None, - vmax: Union[VMinMax, Sequence[VMinMax], None] = None, - vmin: Union[VMinMax, Sequence[VMinMax], None] = None, + vmax: Union[VBound, Sequence[VBound], None] = None, + vmin: Union[VBound, Sequence[VBound], None] = None, + vcenter: Union[VBound, Sequence[VBound], None] = None, + norm: Union[Normalize, Sequence[Normalize], None] = None, add_outline: Optional[bool] = False, outline_width: Tuple[float, float] = (0.3, 0.05), outline_color: Tuple[str, str] = ('black', 'white'), @@ -202,6 +204,10 @@ def embedding( vmax = [vmax] if isinstance(vmin, str) or not isinstance(vmin, cabc.Sequence): vmin = [vmin] + if isinstance(vcenter, str) or not isinstance(vcenter, cabc.Sequence): + vcenter = [vcenter] + if isinstance(norm, Normalize) or not isinstance(norm, cabc.Sequence): + norm = [norm] if 's' in kwargs: size = kwargs.pop('s') @@ -288,13 +294,18 @@ def embedding( ) ax.set_title(value_to_plot) - # check vmin and vmax options - if categorical: - kwargs['vmin'] = kwargs['vmax'] = None - else: - kwargs['vmin'], kwargs['vmax'] = _get_vmin_vmax( - vmin, vmax, count, color_vector + if not categorical: + vmin_float, vmax_float, vcenter_float, norm_obj = _get_vboundnorm( + vmin, vmax, vcenter, norm, count, color_vector + ) + normalize = check_colornorm( + vmin_float, + vmax_float, + vcenter_float, + norm_obj, ) + else: + normalize = None # make the scatter plot if projection == '3d': @@ -305,6 +316,7 @@ def embedding( marker=".", c=color_vector, rasterized=settings._vector_friendly, + norm=normalize, **kwargs, ) else: @@ -347,6 +359,7 @@ def embedding( marker=".", c=bg_color, rasterized=settings._vector_friendly, + norm=normalize, **kwargs, ) ax.scatter( @@ -356,6 +369,7 @@ def embedding( marker=".", c=gap_color, rasterized=settings._vector_friendly, + norm=normalize, **kwargs, ) # if user did not set alpha, set alpha to 0.7 @@ -367,6 +381,7 @@ def embedding( marker=".", c=color_vector, rasterized=settings._vector_friendly, + norm=normalize, **kwargs, ) @@ -463,14 +478,16 @@ def _panel_grid(hspace, wspace, ncols, num_panels): return fig, gs -def _get_vmin_vmax( - vmin: Sequence[VMinMax], - vmax: Sequence[VMinMax], +def _get_vboundnorm( + vmin: Sequence[VBound], + vmax: Sequence[VBound], + vcenter: Sequence[VBound], + norm: Sequence[Normalize], index: int, color_vector: Sequence[float], ) -> Tuple[Union[float, None], Union[float, None]]: """ - Evaluates the value of vmin and vmax, which could be a + Evaluates the value of vmin, vmax and vcenter, which could be a str in which case is interpreted as a percentile and should be specified in the form 'pN' where N is the percentile. Eg. for a percentile of 85 the format would be 'p85'. @@ -492,11 +509,12 @@ def my_vmax(color_vector): np.percentile(color_vector, p=80) Returns ------- - (vmin, vmax) containing None or float values + (vmin, vmax, vcenter, norm) containing None or float values for + vmin, vmax, vcenter and matplotlib.colors.Normalize or None for norm. """ out = [] - for v_name, v in [('vmin', vmin), ('vmax', vmax)]: + for v_name, v in [('vmin', vmin), ('vmax', vmax), ('vcenter', vcenter)]: if len(v) == 1: # this case usually happens when the user sets eg vmax=0.9, which # is internally converted into list of len=1, but is expected that this @@ -544,6 +562,7 @@ def my_vmax(color_vector): np.percentile(color_vector, p=80) ) v_value = None out.append(v_value) + out.append(norm[0] if len(norm) == 1 else norm[index]) return tuple(out) diff --git a/scanpy/plotting/_utils.py b/scanpy/plotting/_utils.py index 8239ff4dc9..9d7af70f5a 100644 --- a/scanpy/plotting/_utils.py +++ b/scanpy/plotting/_utils.py @@ -2,7 +2,7 @@ import collections.abc as cabc from abc import ABC from functools import lru_cache -from typing import Union, List, Sequence, Tuple, Collection, Optional +from typing import Union, List, Sequence, Tuple, Collection, Optional, Callable import anndata import numpy as np @@ -31,6 +31,7 @@ _FontSize = Literal[ 'xx-small', 'x-small', 'small', 'medium', 'large', 'x-large', 'xx-large' ] +VBound = Union[str, float, Callable[[Sequence[float]], float]] class _AxesSubplot(Axes, axes.SubplotBase, ABC): @@ -1185,3 +1186,24 @@ def _get_basis(adata: anndata.AnnData, basis: str): basis_key = f"X_{basis}" return basis_key + + +def check_colornorm(vmin=None, vmax=None, vcenter=None, norm=None): + from matplotlib.colors import Normalize + + try: + from matplotlib.colors import TwoSlopeNorm as DivNorm + except ImportError: + # matplotlib<3.2 + from matplotlib.colors import DivergingNorm as DivNorm + + if norm is not None: + if (vmin is not None) or (vmax is not None) or (vcenter is not None): + raise ValueError('Passing both norm and vmin/vmax/vcenter is not allowed.') + else: + if vcenter is not None: + norm = DivNorm(vmin=vmin, vmax=vmax, vcenter=vcenter) + else: + norm = Normalize(vmin=vmin, vmax=vmax) + + return norm diff --git a/scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[pca-na_color.black_tup-legend.off-vbounds.norm].png b/scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[pca-na_color.black_tup-legend.off-vbounds.norm].png new file mode 100644 index 0000000000000000000000000000000000000000..79701cba60ac06f8712a51e749e052b1d90bb396 GIT binary patch literal 23781 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 AegFUf literal 0 HcmV?d00001 diff --git a/scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.black_tup-legend.off-vbound.numbers].png b/scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.black_tup-legend.off-vbounds.numbers].png similarity index 100% rename from scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.black_tup-legend.off-vbound.numbers].png rename to scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.black_tup-legend.off-vbounds.numbers].png diff --git a/scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.black_tup-legend.off-vbound.percentile].png b/scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.black_tup-legend.off-vbounds.percentile].png similarity index 100% rename from scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.black_tup-legend.off-vbound.percentile].png rename to scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.black_tup-legend.off-vbounds.percentile].png diff --git a/scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.black_tup-legend.off-vbounds.vcenter].png b/scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.black_tup-legend.off-vbounds.vcenter].png new file mode 100644 index 0000000000000000000000000000000000000000..a18b111bf405c82da695d1e5011d78bd96647c89 GIT binary patch literal 40153 zcmc$_*5;R3GTt&-61%^-66QUySu*RoO9)O|AG5q zcV}jMX1eRCuCA)CCsbZm41j=-00993kPsJ9gn;-Y`f=O|2lMg0Syuk@ zYvSl)U~dc|W8i3KW$S2VZb;&6Z0}%hYs1RG%E0!O#LUsr&Viee(fa>xz+h`{%805w z+Whelcsp?o2M7ols(&9y8Rdlm2#9o12@yeM*YuNBmt?Hn$BWDJ(L<-dckL&NiEH7G zG_-Jc3sP(iW-oo%GS$7?mvxpQNaHGXV)Xh#}^ zEzj2RU8h7Y*1w~R9(S2V2w}O@#mck^k_Cz-sB+Gb|2ve$vM|h7EctgzMi|29>AE)I?PcaJjdEZ_g{O_$Q|y%x}Dp9SFMCWo^WJZX)DJ| z9+WKW%=|Gr_`;9t3jet-D=H|;chI)j@PsUJ-nJDOjQ-;sT;9l0|+FS=j@gh(4~*T=PN z+XBCq$z{{dmXzjIS8yl)lQ_RaO)iPoHU(X9huf1UpK7JHgry~2Rr?cdoBQn+|NG;aJ;N|`8 zA6l=YL(fm7p4YcJ(1IkG2&5w{ek3pA1A51&Ur-J$-kLB{Dm~ied%-I?wXOvFH0# ztzi@qZ!)Ltn(uwJI#QdiPe6qV?d*_q(*gqx4dl0r4TmPms}xq#w*?Sy!T-cLy~cc< zcFrRt6Q*3Mv^?3{uq5)v&d&VN_dPIjCB>85J%fCSEOBu7sV!&Rxh+{$Nr_<5^vA3J zbAN4P2XQy&df5lu?Z5ur>rGvMUH@NI6|g3Qt?&OpXE6>dI^O?3jriCbq9Xq*ZwulN z^l$S2Uw8`$eLHBA{Qsa0t5#h$drCWdC>VKa^lwJY{!M>UE>X@%tZ5sW!)l@P&wl2Jj>~Fb&t`UMY1q83N91Qj?2E_!EF+c_TxLV)ByIa4iEqc# zX&=*2v`zA#v~oRHaRgPNF`dM#j&!R?^fQ zj;B#-I&D}kuBxJwYy(=gWu9a%wqx22xQ$>9gTW$jbJcv7twb zqP#rwzg?m%EiE;@T#evySmDm=xDcZ7zc7X4F!F(kDdR%Lf%Y5wM@!oLU;gRMQhedl zB-qsSdN0j3^Te>Y9V8^8ICFHD)$)qMu-sssr`O@F?R7g#_;&KP*ElnytXicz=Jy#f z8hFSfmL;3{J%#72$zL4Uoy=_XtL3Fs_@DiN1}V_Zf2eQS$4svMb7}96!b144cqd~3 zW8%{pf<`WDIHb%^5ewYBeng%@c=Y5$f`NkZ-weqrDJv5$o4vXkVRRUxNMf?v=p-&{ z;z#@dp68#|f7n7;x5I)YGfPV-{lRF`beA=^^*?Q*d0f$$kc6p4Ux}hvJ?^Y1Wz&l( zL8VH=f616F%*^x$ei5pFO^nmQOgM^1MS?k;EyL;beS>|L;Lq`fv#@$+f~ncYgY5o|(20#yqx?JP}RO-(Ezk26HHyvSDAUMF47i=)j) z-^!F>d2|e)!A}}9xEf!vpYBe}8p=wQgGqfXD={9aZd&vE8zh~;{JRT$DKljsYZ5mQ z7wKyK{Y}mKPQFCBs6Go686)Y`3M_$_xQdP5HcJ4;4?6dnkveUj?!cw=#=El#p_@5| z1bx$V(DUmiD(^Sw-A-f1ms`aLXrLkEPkH1AQtEj>8jo}BfBBdS6hSD&zetK-v6T6( z!^qG=D+TioWxuLM@-Yar2`UvQII@Jw2Z0VG)XTfTM-hgoA;*pfPK!YF7G}I4GPveA zFMP3?$puwH>3;^4Tt5Drq$@*?2xI!#G}E6(jH`OGplH2Q&aY`VT^6 zNRc+_Z@E;_zzrOGrakr0LS-~5`~Wh6ppxeyzIy+`5LlJ!k&yWKc!)1EB2IG}rkA6v zf1^`UNoCZ%>HIV2G#U4YCx%~J0Vv$owPhZ2)bIHFIjefRW1AGuXss7H$k}6qZ z*TU3~alodqz*+CjMoVsH5YR7{Aqq@oilYTG!EJ;cMh8MSXA6B*5MH4YFy!Pj!d4X2 z>m5&uwK#Vf2I8E`C3Fj(qnO^EEPj7~x&HooGY$O-vRw5zN<}2$sHr}%k)+S7S8Gn+ zR#UwGuLKkA*2HtPA-9MiNg=hF{6P_5qA;W`@I<|i#aV(RD1;w*Nd%5dTWSJP(+$U| z@N;i%@b8wmV4D)0be10g!thglroj(0A(h4FHPi0?Tzr`K;lbom&$z9TPX11$x2zOe zWXrme=S_|Flgkr}{TvOtaffFvFPP0I-<#i>JC=yU{Gtg54<91f4C+Oln6{WQLO2v> zM*x1-vWuq|RQE=ilsV594~_F0?tC+b>V;y&+x$(K+TwWF?eLdZ%-Z_YMRuh~F+&m) z)J3o)q&0RtQ{@eJ-f*b=kcRfkQXxtnzy?IhP7?6<)0@G7w>0O#{S6I5AXGp%0H9fa z2`15x1FdNC3JHj%lKU3S%89<39$>!)%O7qRNo)6P8nxu-?__)XF?PJL2nQqY-p=cZ zWt{)X(EM0H7O%w`w~*;c>A&(obei|+_*n5DcDE@2lf&8j!^WMnuG z2dddpR==ufhjO5gxs&Pm?Igm3Z(>4}wL*bZg9>2#ZI-2Sh&Btu9b|-!b;GxftBP)13TWB3}inIK2K5tZVl3yGs7y|vr{TB+=-n~3XfG1*3h~VDq~vwkAChr32KE5*}`(9X+cIlnr_&zQ(eA4Lv9x?R`s3- zHFd25XD-0a4YI{OaAIbOc>$`C)*o=F}HcIG~tUwv{E})T?J932OsyR$xz-pnwMZI~svx=T8n}*a`B|Prk+*;H`vk*-iwL5Uxx`aF}p`9(8yD?yXKNnD{m!<(Wa)%td2UN2&$H6qwg9 z%C!ub`7q++#W^DlU(qvKsbzV1p&8%4;vF-a zvYR3Tu5;6C8xyMBsaOqNP;;1m{#g{1X3SQ{Ji1Sg@!-L0qWBC`AJa|;ARUSNYbqRa zOtKETCma$Hoio%}4%NTM4w_xIOT~0dg|2SXm~mN(Xz+wF=dI!~pU?Y$Ja_)4#3Pu* z)+Y|=yDeu)%$xJEULKo%eLk+dRL>apfS2jRY9k&|F#?IPPm~@%)0=|S)x$aAr;aPE zXeOpCt5Vbxv%aFRnM>Chkgwu(rD1f*XGWA!|14lgX(AW@1}6O)4yFrubG$J88$98hO=>~1T3avkQ0s0zMsy0R0UBe9I^MT39D6=R@rizQDih8(-H@?>8B2b zCZ3r*5>jG|!!;Hou=VVGg0WylB_Ut8ydIJBC3K)a2vYfXf@d7v3)L_hJZd*@q` z0ObIm;3Wv$ z^N;o$DD7G^5iyOscmf>M>CfWHg9*dv1LpFmlFQ|Hl8TwB4rqd-K>$HQRP@V53cpB= z1*H{7grgsab-i1nvGKSzaUpBQWSK-{tL9`@QRslw!8s692y=c&M0~oyqGd4Y52lRt zUh)4%M5V+LMYAJCKF>J)cI0p*)Y&8?VF>WH7`Cr>kd*6Ah-pbDlr0YUVdWFrr{K<% zzOSCPy!-3lQyJGaMxtvy6l=@yDrAM-ZO{wK=i5L_9U;~Fk0zVK#Ll1cX@^Vo4 z`Uy~3BY^=Wej=FG&)VdL5aSzGQxR%!2kUWucJjfNARS+=TS83GCgZ(s=$uxtxQ8^{ zX&0c2^6maaXXUA-<9gUL?rj-NMjU7k6}-3UU!JDN@lyc zjKO%QCc%+*5JYYPcZ!C~mcyghUYmGrOE}i;GS7N3YKTv}j4I+=zD3rIa{O4qGSpj^ zgm%vnogYdL_vue69G{?=Qg_Iz8D+`7I?#hipOna4xY3kd!x!Nu_t%*w|5`DT#^e?% zIW#B+iCHpSGz#b+ATBbujY?+rIWRd}5EGob&GE%A;4309oMp%`qL>T=3(GGoglwSX zvRxqOP>yQ5Ows5W_rpazf<*&}xt*feDp^(Kmw-A=;?P7cTt%+zfs)P#hwG+1#4GO} zSJ&wA<>(3!X(|SL``4lU(HeNE zbvo4PN-=4xB6+3x{>*pC<$)=Fi%;^1Wcjh)&)}Sw!~sxYK_1Y z%b1svdmQAzQqRoSHgOLIcuT}92U3r+p@&IAld~)qN)kPHlpxVHTo!4WFx90+M$NT{ z^^}6h5z4a&^TgUy)?bfQ`tO}CPR@&z(v87=YRvZW6sjOz}ty?g~+< zLt~oVI-d+OXB9cwrKWer2XM2*Glk+>q17H7-=PkNk#nY)$)ZiVecs|m>nIM)&NOp} z(8|E_v4NJV{S{3ffy)sskEwa9`PGK7-4FHgX^rZ%xN>RY;LY z%&`?T&ok~4ZhtHe$yv2Low@{P@Zd$LvZ=61;DrLaJ zHtTS^;K--7Z(^rL$6kSk2_oT@7%AlSrUXcCz3$R$+ljK2IlI${bE5u|`1Z=|j+Lzb zdvVM-PwCOji6GBiePI>1yVyTDCv=IM-pd0s}W;Ol?M_o?GYAd$qu+VU|mTri!(buBln7m z6sg3Sw1oEZ`>ETtH5E5KCMRo6%I&sCI)A}`FOz4u%0TpepE|Ambyf0>*@WbQrCw*qS!KTp1IB1_ASQM z79ONYZ)aprzaE>L+eTXgGOR0<`&M@vf-W87;H@ClqEKV-0Z%*7&IYu8$?g+waDK=c z1}w5(KCRsJ+?Mg>s*nzhD06@mWn{SM1!F3T#uzYH5UZKc=;fqa6y$3NZ+_)i*Hj0* za9aY5KT*VwTp7wbuO)HKj3`ShiZcKn+bvAn?UR4yx)%n~Zq4%e##-NbbjjkF@VjTK zne{e&M=0%cBG=-(;*2+3>G?%c2VWmy63W&jUj576%U6@CA5-khB0vTOU1EckP9)to zmd>6=*^NcHHtlaemK z;2Jw8NHyK8?q@wr=C>6`=b0U&k>P%ArsUB1UtMN4#IyL4u8&Q7SlzRvekPD{q<+&B zQ?WP{&8q^fK0nhRjNz7Sg42VCCW4zD?@tc#1~g5(6OSllaQg5kCZnm)mQz8Vt^j`* zYGthcPII!=gMpYWV99uZvj8v1QjrD^x37sOQofLQ3qO_&UnpaxiL*$7s~KKuY8~Z^ zlB9U+;|0=HY~AS?>cLU1srL7H+RrC>(jNS2{9}()S@dV! zy)9>5wmh&13>y&yZ&3wW9uDi}K37i>+TN_E<{a}`f|r4fvEOer@!9q-!aI1oc^J=0 zc2J0^qxNzQ^_^rXzZ7(!)<5c1%q z6`{hk-N{b?p}3i zjf@@v@=n`PPdcT_>o<4H=NFApnve5i8IL0#PTG9jk+G#DnvnwY+87Ql##b+CdKb^; zfL1;Snse(0ro(12QXg9SJq2x-`|H+Lb;JImvWII}1|s)6UXCYs`EN(qHyBjEr+H)b z+HQdF&rKox-dEqR=k9k$Th6G`y_lJGu+wl7h^WIA>X@6kM085wjRZ)rXXC^}4NBPL z*Et2jDwOY1@#t}y$XqB~lfB66+dDS(n~X@PwzXTbW{Wm8F}lVA_)`K;HQ((;Ib*7q z$M1lM?Zv*uBfko3VdwUg98AceRJA7WEQ6XcmqdJ7+CJ0P`~X+9@_ifoqZkb ztC~{7g_L3deTay;)yzau)?h4P)fIfkdH`r9Zv7H_GO(K^VYThhn-3AY{F7A+*r?gR z#4c%603{cjNyhBUbV8{}IG;Vcpn&;V4~*Avu&)y$cywgT~{ z3VXs76!K69SMzf@u~Xf&Z@Ndu*&IJ}ivwLH<-uw~3WM})zM7LY!rT<5x{Dob!9eOH z@X2e69OC7C^M3F+esrrt&Q#ikzE*U{{%n7C#sgED8_iOccE95pZf++3oa>tWc~4W~ zTy0fiva{;G-laB)4nglhqBk6WnQEZf>Dcw4UC-fIJ@Ahu0q(+L7+pT8+2O$F`IWJc zY_azROI?+3L%`nDdygVcm+c%{0unAc3k{y2;jHT1{xaS?!9;_5h{q>r8&%O59EmN!^H!IWA`30;2ob2N0M4B8pfF&^;rTzIBg8hNmbJU z>wCTE*3b`bP##c%dCSS;Zu1@!=))TXDoZcDJr=PN73QlCABLyE-xu z1i@=dJl?Q)jh766>=Kl6qruK@^Ep^SM5Ie;zW)S;zlZpA;r_x46B-QqexWE z4CF=eb)P!d+HDFspRdW7n;+i$*kv6~7db3Cbr=|o=m%wZ@X+5$;Cko|^oH9!D#YeG zqgQrP0oey8mw;@{gm1;6Aqv0=!$K~^Fajf7rPYPT^W3#t(lc?c%|Xp0FXGxV+Q8!0 zXreLFZW`uXNW}I4q9JOfgV4Ca``<2N$Q6&IevuNrqv(=CQenkMMROwB-zvY*ofxGmBQN(IDQk}StDN(<;b){6~ZRBlJK7009um%NMo}~bLiw^ z@T~VfSv4bz?%kEq|2zP~ghFmI7q;H=Fqve&Wr3#ANJ=scYU;v5(hNOq{zCzt_n3Sz zg~Mfr(n5n3EXWH1m7NTLC8O;P;{S3upu7B|4jqUQ8y7+qk<1RSXvAC^f&Bzy>}T>> zSR`Bx^*0RywkZ_Aay@&rIoc|mxa9MS0w+EQSZW3d*{!*?2OYL7-knLpWk#BdBcUJy z1d-ZAi^CHv*uULG-ds|puv=o|XmcdY&8bq#WxkNdw`ceXh#!V# zXN5EFkbnq8#nqXqz)X@0{$%vnG0}R0jH;yN);Yrsp=?H9Rv=e4QB}JHdULa(N3#{= z!Dw8C6c*7#NU7!xbT`OhuF}l07-S&`HVXNDwml|*nA3U%11bGmt!Sr>UyIb@(*^{q z;X(uKX6(oin#bv*K3*`JAp{CAWxAahs)KbFW_Kg`DQwV2EQ${F6@vV$O4D+K?EmrZ zKSH_RLYKHzR@xxhS#5N79wv5LGm+2ceb_fCDXotJtQ~LrG9gb&J>UD+ zzb}`;m0^BCA!ZaISO6kSJa{&&^B;JT`9tVK%@EB2*l2;t6MRqQ6Gy%K)ziiGlM=D0pFe{j6ntB) z2oeQNjl|mz1-R5^RcqUFKK1w{8ewA6GK}U-BT&isvmVwoWfp(>M1t_CT0hxUiD2Ou zf0xyHHVx5@uG7=gvR;6!rmM;{o~M1g)Qk4KKT#{ zYN$Ut0*3VuLBV|t`^f+8E^~ivE6(w-!qE~UZAPx&oFriKoE3FX?G4Mp?ti|&3ht|Y ztgrv+q<7Zo3Bno0r@yUAu$xoqc@XpBx<@(vOMu0boU9;_@Hg04f;{7SujV9gOxyXd z$lLvbsrHZa#jnqgx-SwCKta4xTYv5cS3g`zjcjV3VEa2H$! zQVark!`+T6HD0cKX*{{S=s$Db4`hW*?u4^&^Tf4(ZbKCTKrjzU_>nB-f7zLXP$-L- zEucyz7A$8bEs!*8#oyu%1m}r#SCPl>2~X?iY88=;vBRt&hsGBR)qGVjozizoDH)G6 zC}ELL#G?G0N2(vTEs;F&S!7P*6v=!hcBLNhLk>F!iP3LfRvL5Y=)7~vixy^?0#3*S z&PK03e9AaYqFg6X2MVB@LNsO|S(bNmh?z@D5@$Y(9!d9L#Nsw)P`JvI(Nee`+-JjA zd9L|3r&LhRr;^!#2{LePk9r?p9?I`RPE35lU78imU=3ql3+ZW(#Cf}Q@%H&k_;R?} zag`+YKIpUV89Yh;xAV|(a%HOjfmQ4(Ykqw%2G)BZPmXWowDBbtB&8aiksxSa2Y+pA-Db0BNa#pNxPD{CxI zYku5}4PR&(G~Nm(Ip=Npd7Rl6@_s^NBg$+q#FH$sv)kFuG?rOl8ZJM z?o$n^?|4y98bh2SY{lZI5^EMp%TE-@smTHglF)MgzWpyupPfXfro8IDDTSDQ?&e0T zP!p37^^*mtE+Kq3JB~B2M-UM*k*<-pv7kEyP-^R>C%f!!{6&dLn#A#{5<+S~oG{~o zL=EqE+uc*6mbS@Zk0u!+F|>);vJF)NBPgul8p}6l64L)bh;yuC4_ah(;2qM1@~6Dx zJv-O>U#hSt?7`p4Ri8%q+|N1@%w1sx93rjtlvblW#D926ac6iNQ(en~^l~3Cdrk6H zhYM3A``-S8Xa4B!qWcem(HZB3n;Z9^W>f=@h*x)dZ3^q&sQ^du?aQx2@{b^ zSkTUI@vpoA1;e6EQHW!6(n30~V764WiXUK|^sh<3nmt3y2_@ohLlx?#hci#U9eSq( zWsjq;JuW3#53YFuzde3kU%A9<%vv_1Fa&ZiR+``B0aaQCj|C+_qT0V%P7PQ zoiFt2WZ#~C91sJnxCwG(l{-yW4N8BbRZ?j1Rp7N+@gAdE@iO3Wc|X!-AWRs~H7DNu zZ6Q%@?)J9%KCqiN-E_Bgx#A$3>K;nk6#_n(A`GwEE@%3@p-@POZ zmWPJ~xj~spgDLn{KE~b|LFdX2F!p^$QkUM@Q|x8isMRVyx!#$qZcuMpqXw60Yuxi5 zn_a>0NDYe5H>=7+IIlKF!x{4sBnX=BRC+?c_dCSb_4~o0o1uM^Fm(qt+u(Sui8~kc zwIh-xnx8vtxzodgchEU7bYmbv7O5{x*;vME2#Jow?b!e?0WskqA@72eTdzE%~N)3#W_mC8OCX1vPZg0s(GPC7E?zm+Ye!qj2i=l#6> zft(VffW1iR7xe zcx`u&2$Z%!B)T%8$MS@UPsB#fX9iuCpVaiWw;4gEOa_X{I;`XiMhc_V)GAfa$=|vM z*101$jtE-6J_YwCx3$1~A9b6vMtkS@-q$?YP0rf9ak7S&Y^N@ANac@|N`65kSjZGN z-T0$$_EksLp%dk&DqD10T2%$TS;C5 zr)|aVYP~n*FK4FxR?n#j{I6wNi%`v=OIbke9H1G0NnVMkPZiCVV1v1ldVIr0(* zDu=fJaNVXa&da(99f}957v4opXW4WJjzuxEVK56G-e9OdrpTO?f=)@d>xuGQx zhu?B%Qi|2h(F95jsj=466e12=sGqhWF*SuLH^Ifk58rC zs~RYWHy{D5k;96_@x~fk$fGL;C4L-xz+Gy|DFH}#U*PPROgXJz)d(S1Rdjn2<9v(K zGA#qkpRJeEbmRDhbkQ_Bj!(p2M_4xUSLcTNxO&k5Uuj}GFHHL3563X+A2VW_QKGT; zcchLbNjvSfzhxRTbZK0&SCcRUp$sZhUWq!^bcZ73?paPof!H$R1I1W_+Vj%8l2k@K zc&>YXLJX%y#zP1=uWbgyeO z9qSI7q(-Q8N&B`93gL1t`iqzY6gYWK&3t;78Xr?q4WXf6h8j z=EKNpr*9cq9Ca3F9w1*i8qv?ReT|a{b(}G)^ukf+HwGifPaDVp_GA^~ovQ;<<+T3U zAW{jO-VHWVXG`HmzTk*z>zPWK0lkBs6AqF=1R+gSC1aRWW^3GA7c@CNaf>4YI6pY* z=Qk9ORoP6Qdpw4d%HLMjgn@1_K-2VwXzBY*?8HjMi*O4{xr_b!oQC zeLSb{b{Y)ljJ8JdIEY;#(rsEME59pSpN1HSo{W9oINAdAWRJR+2rGcgNclVw<<#bT zK@Kff3u#(zH)^kLMmC>Kkq2`Z3HvcZnCryw&prPH^J`Bhbx1D{TjSj6Pxiwgu#Dn5 z;WsEZHCajX$+dO(i&dTWjbxo|u8^iEbHGqXyGJk2cRn#vFrXF|*UB1ec!PIO6#m8&!44!5sRQo~TqNYnGuK6?C*Zk0H^yii3D!_BnW|5k z%8@13WY)#wOqfs0V?GLKHwL`4J}z_OeW^0Kd$C~1Oz!Q*@`l+zED47~*s;|2@=C8A zw_|#yf&$Bw$ST1J|RwIEfr8uXhBd42@{8eVL{_0WQJFs<57r21>B zE6kNQ%~Gt;olkE|dfgz+LBG%!=yTR{d?#A@=ta3acSdfaIYB zM=3TL7!g1w;X1vwFno1+b04!1?bqUf>Jf*PH7j6IgFH1Z%2OP=GxM&v{Ys}ZUF5ZW z)R0tUWzF@Mvuf|96=FoK`QZEalvFlEJSqI4M#~2a0orZwgO$-w1^+npRq_N#fCAZ% zz=r7FpGZ*Q^YhG1s9dC85#V~_?!VoZRc+&eN$;^f9oOhCdeMCm#?}$1)}-KiEG0 zjlk1>N=sX8`hmgKQ!^QY+<;6EzIg!ewKoNX>!oxraVE*XY{(z=G6`wx|J2V9U-X`E zGu9N{Le&9A%tEt9sK3N5TBnH3*&zB>+Ckg2&iPhZsvj=CGtn0tCPClqkO=p76 z&B*JYRn+@+L);d2C!7#sY;@p2PuYxfhSnnjgAsA)tl86u)RX`T1;jU5ckTfMaU_R{ zKhv(|`$z-Kn`EQp=Y1*@7+yzE2e*%oGCsSVKU`Xt7dEX79@p&KJuh|od26F9meVnd z%?k+L}{kQ>XI{#; zhzO!aRO0WLG}dsW9;D6jz}QbTB3E0E){~pVL8IA;A!9$gtojVbgr+&ew_1PL3376y zv}Z0X#NdY64A>HdtlAR=4*7KFTb{#WMhraEl%^jwVDoug>rA~r!M7L4ILb`RE8-Z5 z#6|J`fE?KXN6_ius&Z8$$rh3!?|UkOB%~rvs`2VzC`L77Z1OxZqVwTCk{PNNN~uR7 zOWlHN%hdoGGDv35{#D)FjM@+l`jzkc)F!J@tb*GpG?ahLkr_y)7oaqpr%>lnBh|90 z!t%v;LP+cUkUPGjM=$RqO@*MN)d4O2fLh)X->&e&yrDNm$W!)y1oO(?{?f!gUkKd! zfxglPc%2jNO{5N+;(O5@W{DeQ64Vylj>@ZoCmZ@yVQL)rXiQFmdJG0Jn=zf;Y!POS zFlMzz-R$fbai80&HokX+@yE`;AG2>AHE{o_I9~Tbzx>A;aeF!}f`8&Q1M=fX{_Ico z%Vz&^w`j_0Hjy2t(~4mjWs3i~0TDjfgzS(?0Yg1ZLK=dl1ez(Dgm%D8Jl}9$G}KU( zoEA3_x@6BGL=?w>Lr2tUgSDBvlIMhZtNI(OZf1C+xZwSyJ^B*v9y({(SCXMmHQ5_P zY5qpx;bv+)cL3{bIM?7p!VLNpgnvv9x6~o|y|6JeIAjydDe43&`j`E}U z-$}%r5;4i?e1xbGu`GvRZ;iVY$|eZBmw_#T_ER@by8ZPE>YRrlhX$G^=&de5Ri7gY z6jsrlefT*RR$PD1WHZ!;s_#2~q*<>?8ze+a`?Eom!t##ET51NIlUC@wDh9`KH354a z2+RHiU-N~<+#0PLU?3t;g?8DCsVGBs#^rQ?NUwY^Tc~f&8VmgcL_$N3(YEL)RO#Dl zyL#}hqoJiGLk~^okU$f_5qr)ZkF={lwtTYErAW;AGHYVWkADp|sKJ54%bC4_QC<>^aVY5;_hHruS z-$tfak#t$Ys4cX`eEiJ$!qZK(F%hCbrZp`@t~vUt`Nq5@u7=}cu=fR%rm$e9R?AO5 zkA|u2@Hk~c;4)gCjctH8o~EiwKd!Dm*hMQ{Iy@)^zr`y!tm=k4H9Oq;BL&Gk zOo4|xyu2V%9iUmL>rNyQv`t)!@gkB-4h-+F zn%&1Z%?=uAUi#K^?PO&cKZv)T%{419)vBr}jjt*8{0MvW3@%d33rP|~y^gLwo(M)& z6?B+d-iH?-N(pYZx%qA6pLLlLG0J%9&_zkbeoZjQ_=A*gA~jwp+A7i3n8nPLoQs}G`#r~+nY+Yuh+xNdJZp2wfu^3c39|sSuwa~8!^blWtvTfW zh9mv|@Sz0Gh+di0GIC5eKcRRjLa*F1e^NL|FEpM!!|gb*)L-hSj%y}ZR3|2%Ef*w5 zP(DRwCKDbQtwEi?Yq(mpr}B?=Vw(+&K@xS>=E~G14@con1WG`A*2S zCVRxX@3d< zh3G7Xn{L@jm>2^rXhUe{>o4H_D$RK7pukC|m6o@|g#^yz4;yNf6V03a^6giAv^s|a z;N4?GC;c|~*W4ptEZLVU?k|y|J!72f(u#`cPEJk(o;n{KmYVSMRG}do{{8z|W+UN< z5tt*5KUAVojxlE1T{tQSSrtYGp!l-qv}6tZt@3fvyKflz$s)0UPgs@ErYpg)$k#EyX%LwSbU8e!Lmmcq}JQ?WG>EkzT9sViG^d!L3IjL1}Rw z#o%2^0s4cp;xRawFv8f8j-%`98y1F8Q_C|qa3mscq~%1b!E8z2R`g5Xx_(rH*U?iY zCR?xamndlUgNQ#;O&AsSkWgf}e9-e`fC{6UZ zwF!k$uX3FMRB-3!9=2T2)?jQNi(*Hlgz|N@JA4~hUe;jR` zve^?{YqQGz_Oj2Pbz=CzpgZ%qM%Hb6w{HGL5aKXqBCQDtg0xj9{aa}erIhQD`U}5N zBvk{L3IWic4`+h~Hd7ui?1m}D8w=xc#?xgrZwe^ZZEe@bSFT7r=2pyjU0@y72r)6_ zAo6HDTp*3RA0>gECC}x#jFEMHpT*-k0eCC{-mgzN(}&2Z->!o{xI9P74u@^~*P9)D z1+FV3&h*RTjckBOKD=NBAcLTg4Y7jeaU!+stm2t;wC((6=wNc)C+bgU#nOU7flFIw zFPj}MUqXs_waV04T`-yjLiVCmC%9ttU8rTL@-ngBlZDwxS?z=daFR~NtBNbP~$p%ifQswAdimjKo-@2`QW{f`PP#lE@Q1iBoI{UTuf#Q-zA$gr?yJFl&&WpVT=66X-;{pvSXDB7o`KklV^-6+LxB1zRo!3-Wz^3XAk3*T{JU@4jxv)<`*DN6 z4?_y_vyYV`DyY*w%9;9(enZIXO%CR|sOj4{q3P%feZ!{$6%{NI=S`FrF(i{nVdPb` zUKS58#4PaPv3C_UPm?igwtI3}&V5wyNvV`As}v!4ghhtVM;LU~RYfa`O=>QRS2^;I z@UM=hm;$Ry^CP|66wQByCflE~sV)dYCI&WjTN1MX~8kJ3mv-OS(y9~9b#liii&s*Q{R{+&b|+kO9{ zeY<_SM*W&PMmlgEt^#JD*|%YGrOX{aX+k>AS$X}*7>LprW*7>B$6UV7-ACuWnPi8zlbeyAdlA@u2S&uAJaN~Ea&z_!5$8Veg4 zZ5>Ug@uTxv$(_BM4Z4(;|I4}z#;;PVB6q8-QrQgd>upRVm_HkSh<1OpMaa;$E+78c zA}5h&+gQOKWy{BT0OLpPDOutA6Iq)gaOs5^8uF@~yWyLW z^jP(kL4DG-!-i&-MkrDI<)zX{0j+$x&SiGxu?MEyUpXI3-b39#2@jN>!#HO<=ig ziIJ7;-%g7>cyS5R{DKu2R-mYy0%ySE51+ z&Xx33MN*WIav>t$1vPCDXfy^Iww_(g7p6tX7|`rs2kFKQ=ARH?GFH#CLy>)s@afVt zUe*vsMF~}|=UFEfhm%;HS+c){#v1ahmihMrFGRr2i#JwwAlx(?<2D}Y>b{?>v<9dm zAC(*;9S$^0T`N5OVsyo|5W)vFT|0uBzWek9SEnd2XvcRY3rOwsL7jbP+|3}<~N zaca!m4}TjBcJ!2~?D;<=U1LMeGJ`67`9Cv%0c_`Df)b&MiWIPU%u>o!{*?h)8*QO4s*40=6Z z%F4?zIsXl??Epq0zy>UBZ_gl8j1q1&9GfbvAu|Tew={|zKA^^JNb0<9$-u%&l%S%3 z5;D77Wo#2oLZ%eFau_i)nm3GXt=mF8USgQc*M~`IqS6qAVaB%8p!sxhEFQn@O)Z+$1h&S>_q7{a#Q2maFsZD;V^buW*h!bB4(3;_&=LqNm>K4SE+yb)&j;#jrMJ(bC1+F)h# z*4j>RcBAET-lD~7yDQz;To1%h(AO$L5eFrQZ?(XiWKY#juz#|y0))^NR z71dd`k4NXmTCX-ks;zaRYI7LQeI!sB8+15sLk$YS?x@hT-$l*iwY@WxgC3k*@l~;R? z&#N6Zf&N*o5Z?NI@AS_+4x_)gyC7V;#popz-HC&wNdvL938+X;gf;r0gQltq$%taD zmv+NPF@*V%zLOa-!g8Ma+y9vGNXtX+q{H-4vWF{nvC1Bk2_G<q1j|UVC#2OMD^FL*asmG;GL{PV(xe6s2{c2WWI-J`n zF!6Pb6jutn6B+z5C2`gJ!Aeu)AMEf^=%IN_U0g6}MSfG{_dM;EqmR_YMebtt;L2{m8piC`(Ul1@R+p`ToUBau*~UOm zP{!uY*(|D7lt{x2V#UCjrQzGdwUfql2H? z)qJOE)v41yf<#$=6Ztwq+0GB%KD_zqkxB5P<3q&FoQNc}2q*`~)(*~2HoM8gi^x;l zCR2zn9YJNTPGIVI`RPG;<2;wrbp9gz>$APkLqT@vJ8tRq$)r`TTKI&zR%;pVL z`*!onDu5{0CvWE=hl*&Hji^xYW>fTq0*r!y;-Y@d-@oo-`-`;>R$G=Ro5Sz-rH$X) zEYY*k7sA6$c@*5Q*ZnP%QC>zakEKRB;mIm2(#$cVRk5AmHh-`$PI_s}nfI zQfbU5o{t|Jj??xxgFo+Pgb;R|INhHQl7R!Qo`eMPHGXvhxM)tcI=KKOGrJWyZR(vK zENmNg{e&P3OH0GG2J?Sc+u4{*CNpxn(R_#mh~Ejm%&NhL5Blp=7kfS0w7HxGiRACJ zomJ(Rlo&Ry0)yj3DpPP4k7pgAowTs9016{BOA`W2^q>I-EF>Vy8QmR3MMaIt%$(eO z+z^tHkqNHmYFHMu-fT;p=KH`&9v}48^+x+G|2;_1Y~W%07Dt+#(JIwSr}TCDv0qe6nOLT9fVCXx%dv5;IsHK}>AY z=xY;El`mi2eZ87LnCf8t>#j112q=EkRh+j&eK&K&G2=4DyU&#c z(h3rRYgpdfn}@FB8mzRm6nKE~9#J~Tv39Gu&}Unm(WI9k(sZDmc)>$=NhZoW8lt8N z-0iWI!7tdcH7KAt)j>TPWKx*H_S+=^SGNmM(c;i4Ev`3wp{y@4_ZRi?VMwVZ{UNiM zLy46H395?gGXoO(;5?oWc_q5ECU1IS6}r=A9bK(S?(LE*UDi-Ep6Oyn6lxm++$XFR zK766fU$s$%``CpTccj->HM9p;Uh%k*pZ$k}GC-XNo7gL2;oV@elaQ>|=0jNvJz1TU zDKN-pX(ZI*Ukgh3xr_cXSlnDh#^mb`SZ%#GEAxkvdnvVSK<-VI>;=UarHZt*f#8)Vu9|Z))VLH{O!21Dxqmu@WAOC+O z!2!T*TgS+BBs-2v-dw$7ZkjBaZ7QI@(UkY<(FHj6Yk3^rso>aGUh!6 zXKJvGM@9?A?^v|3TP~D;fjNIYyBWXq`F%G|A1^U46^#8N{&&ujw$}?)QOE0N{wlwT zJ-nytT5U@wK>!_DyzyM!I7)u;W)Gfk26sHVuap2H>O`KHBZ&Zcd={g){w+T3VZ?ZJ zSL;8es0+na^H_|7!^7K`v+DVU1*eU0O){geF}By$aVWiIIS1~&t|AJpLRC&UXhl!>Nn*7>!q-zb$yR&hny zC^BPjrmUocb5q679hzrVmIF7=2dtf9G!}=5?rcQbg}5<(x;YH&*m8a;q;h3E+N7hb zrV~Q!e0ne&#mEvF7vwtPUdbL?)bYrU7gB7DUn>*YfJDe*(I}{|FG;0}|CLzJ`0{fo z%Kac@c%?;;Dv`9^%-q`8(Hi>w-bSh)jzq#>-}qZC^D4!$Qi!-I(cc>)GR>8)V$5N5 zgqt}G4{vNLzD!v&L#ilyc@Z5qGu@D|No8ST2LCF0ET{pjoN*RluuNa{RC49@0Vy)O zgs=qEqEP$V!u{OzG7u3zaG5+j*UcK8@F3sZgI2jTGcjmNEsWHMEW|2>A)z;ih;t@F z^TQCrAv(pP}?#@N`{iH^1iXq^EhNS`si zR=u*Iu(;l0@mEl`J)~s!OIVe5yMns<=!ks28c;rfOe4;u)G7fG6ILV!9ApxW zt_*OTYuz$P+B0`LJ!&17X)jw|9e}<=0H|-PU4~F9;orWgHFhIQrymF;Wv`iFZz;qdooK`ZTs%s_^)b5%rP`r@>a z(~TRGnuK#B1sGf);YiC#UmQ72nP{wjP2*LB!gb|P!gbXk5&1$&N(wZt;FT)j z!+RWuQz6~hAXERhO<_J8Qu7)->wq96)2JL%6#`+w|cE*#Sqt*kW~;goT=^+lKmJQy9U1j}-Wtpeu%ZT!+U* zi%OVWC-+&V)VqT3oPOzSb3N+lc*kId@-@aXhI`DM9}9}>L`9AGJ!Vm8J2tsdZ-V)O z>HQ5pAXa_x3>rS7uC*v_h%{YJ#Bx+)ZDMyzgVtL5$X2s^U-k{bTEW4CRk3yaUtzi< zbeisd9+GT662c{W`}}&(`NF1f-TKN>8=NjaN+}ljO=;_f%KOj7IR!LUBP%?K9c>AGVyktluW9$k|?Ut5IPh8Y&YH!X$0Ei!!vmGD1do9o`$P7HC zttaWxHSbmV#LfYLR-G{_Gz>`fr~|KET5j{hIsdIjjZuglr43i_>$1J4!_ZOwoy4?k zts9xzeIejEhXk`vT@%ar3XgJ^2Cg+rmyN(9kN3VSpuo9Oyt}?;I*f+k;rojNsrIdy z?>#mx4a?z21nS5T| zrZ~^2@jQ+RZ8~m5J}VQ^EXC;P=tL%0^7f`HzG;>_*(bnP&;b-RSX|Bo+Dxu`_dAm| zrxSmbV#A*QzW%#dPi}QQx~=wo3nvr`qIWu$%kq79w`tjdY;b;RJ0X2GY8EpZQSCaY@dZ#$Zd6Vn=KA!n+eH5-ldT>^c;h1DQPJcuo-o%v{8@xD!pHk~3rh zabJ#f5R^$-{$b5_3Uy=CELJX9SqL`x~c2DJmysNP*38K3nxd4nx+d zjm*?6!^qM%aY>9W{}G>}eP$?J_9smaCzC)f=0Gn~W;&8?OYCt6mn*uTW6=<#L`qRS z>dOL6c2ceP@;!FIcbE6!&YC?9ouU3ck8gc>z(#`_rB&V|7r{K%j8KK!=N~Y zm?6?&?8mQR0%~JaP0TE#08v z@by!TBtPQJk6piMawF(Sg1Q!aa^o!#8!BFu3;i^}46W~Sfn}iJe=0|%=m0f0EHA8; zG!uHUJ{}eUCRR7GS-#cBVM?0VsUW_+;9txhkGZ@lRJYIbi_@JO zwI6Jbiq|`A&%T>-yaLOamt-e8;l9}kc-?DDc6S*>G5dxj9y@!mWze;bu zD-O@<(LE;XqVg*V*gq%HdB5hcayb(iR=?enXsV*s-}xCnQz&Rem*Kolap}EZ1y3%b zQ=A=KLNW z1N~s~2|E0)h@-5OK|WP(zb!i_rez;T=L;ywEV9Pw{Z1cx(i!m*d4q;Jti4x;AatfJ8oM;JEPZR`een9BlZ0ylHt} zG=8QJ0+Eh2-dW|g38Z+@#9>5$egzfZ`yY}_aeT*ymP>LnlkdA1Q1g*fa2GYF6U+5HN&+6uJ{;jYoi?Qo)~Bsw0u@gMpN{KaJchkH&x1EDyw zZ|fc9(&To88ARk)kAsIJwvU$Bj@|q-(ZyK9&bJk*y15GK^sb0IkjdS{2C@$WeYP@- z4d)|m&k9TW8Y`cvE??9ZakP)zv@B(lta7om0m(RHsIY%o*)M}y4B#5!xVFWWj`+oT zbg_U`>a=euxk z4!8s_e@Dx+REISQLPZ6A5h{*4SPr0NFG-b2A~m6VgSxNGMl_qzuOGnV@Il?Yr5iTE zao7hxuXmf4_PSMJio&xGe1Ui&`GZaOjF4Ii#deJQRvyU0idu#0^W3?#zqLGW%ZKIJ^~15rH5o$D(9rB3 z9T8hFznvzouG`+p42JNLe)fAqgrvv|i_7t8X%gK&Z*I4vJassLYw!p0{Q-;K@z zNdi&TdM^POSDv9Zg=7jo-rudsw3vd-qdTT?r?*Ew-e2*KwD_Vnu^;RPujn|dygw2Q zKuXSQQWi5aGd*vQ+tZ+{?rETb^p*lWSseayT1Bjc(uPDt#ALpC`dL|V&BkB|k|6TL zz9Cr^b8HpE${0lJ6n#4me*lP?kOpxG7#fUn;=pqcF@2P?tsJh z<tT8S)}wValf)2Byo zOkW2B-MOlk&$FLnMR~R9-yXl<3-AVq0||$^`qJuhVxTC7@pz`t=NdBo0)~n~VM7LB zMlP+aB<12tU#&OAw_gtmf>cmcg#IIV?8FHmtQ)y|6=a3IvIwzW)gGu1T^u}2b<>%~8jzvX~C~%PyihlVDLA-EjF|fIRa?KmHL?f#! zn*9EUn-_!tkm5%O^FHG}(B9EplrK$+Jpa2-lz^!R=q`r)5%{F^^$FvgoQR{o0HGHs zcj9K$1i^&V!7CjtfpG`M!^%xfA<=DPKa2uG)T0ntF^@AiGEQ?qAih`dlJIY{9;CY_ zxMY+f1gNCyb~p`@$n&+20XX|9{Cg7MmFgJ+ zt%ru3K~C*|)jOd0YFh`Ew)Ns4A&umEarh4HnJqrwMfdi`6trRyZEaQuZd6{zw%gpZ zEZz{K*qWG_m|{twJ9T79aCm2=9st-JN3|8q?+f-)wP^k#(RVOtaRVUFRY_GfJ7KkT zJwxUnB>0XUM*5^_qM{~lk+WF|i4!jCiag@}X* zTChtlI12pY4))I*0|YK;Xo_k@u@Yl~@4c@~a}8@{MPc|STD$qR>$wg{Tju}y71mIs zp7QFhvA?|mtsn0o_s`1%=NyDN2{{3&2*RI+czWe+vRNH#I^d$vwm%<@B`1(?Y>sUq z;ujQ2Hp=m|`MUfwJ7e$*u>+1koO}bAgCI%VOx}mf2dO}hfx@woJ&>w6N+0w+#3K(^ z%Zi*pJVn^=GRXwFE~Eejew~?&l%$}g<0FP3K%`jUSnn>0n(CO@BcjMV-xgI4;?D=q z1757$zaE#1VViDB3dS5i;qzk^c!>ez3l=ZN?^tpqGwD9B=|>%G=zEpUcbE9vgths8 zY?p7__N1a=K#%xKR!?6xW-rt_e>P;%KmZkjM4b%dN3HQVIyW8y@D$E5BvT@X$^lpT zJB$fE#>nXC4j?z;u`=Dq*Puoh0-W&bfKCW8au8nA(U=}1#t0LbD5=HG8Z?Mv#e$=N zD}Uz9NemQ4IC_g@3XKrle=!_mEG)n;pOBx{s3j*nfsP_Z5OQ$`NAqEX#c407rU@;fet!jCZx8q? zlWF^lyJE!k;=Fk>1`BKJgLlx?QHJBx&Pj3hVHfB!KPM*uMg83I@PIhgZZ(xeE64sn z9Ho^;)1SIN9NDYYlg7hq+MfjP1q%*tLz6X|an&Y70g!bn++$Hx%#ld5wU@5)*Dho0 zC}hBezW&fXqgSM>#4`*1L*rMbZB3UkZTrTV+QRjZOd#+Mq@;KANB*2h1mXb_UPn`M zW^kmn<_AZv;WY(aR363ZDx-+?DN>YA4Yi_g+1a)Zgm$_tc>K~!cFu@=I7 z&0BdE<3pEmGCXO56oQ>^+CR>9JdZivVt7~N1?wp#)~ik9SWKD9C0Tc$+=xH2UR4te zGnO8de9O4ER`9sp=({`KJp^~?UDDx_=R=qGb^!V+ud6ERDBCwnRmc4gsbrFqTQ*R> zf!+}@QbdRwPc+H+_U^W%tgP>d>z(|~R-?fT2}mpVb$fq4Qm-?DZr=9c@_aboS+lG* zzB`5BK zKw&UQ^_bJ(W)DZTp~iEeE_PP~La~cq6q7C&?4LdB)tIf>OuHX!dEKn3x-%YVqdRNk zOt)SVptgPEUANVSvD1InI7(B zJ{*&g8u&%*>r7GqRAHiUx4hwkqZ=Elx_}|NSc4xgz6(tmXIWZOrDStH+nHF4K=wRu z-9?7xkn_{su;mGe3c9N)sV)(FWR&6dWD--Wj-^jj)hIUU`Z}5v|HI7qnE}_<3dWxw zK=6RZzaY0Z7U<>x^e=qTK>sippYbjLL9N||#Q{Y5M`jAmNTGf~RmQ%hhlNk~c> zLXqYE#5G%i=<;ZHzQ>O%_>HqZ*sc<~T1dn-Dx#29w1ftVI~{e2`o=KNDesU7yix9t zbk)$Hd|^R(qvx5^b(TsrH$fY(JWL_yz+(ccWmEX#_Pv=xo~R`mTNK|F_Hgne@4oW+ zq`QSY34^opl{j0X;)=A}SCWCXFV0xt_g{Ikex+JJC{1AVBs^S3m>6RCn!=>2q|#wE z<1}2Q;1sRN`zzoT(s!c?e32I?A77~#GR97JXPcs(tngq9KQ-5|*j}#nddbbKh5_v> zGS_^etjG~wvpi+>(eO~h#dif%A(u%pC^@wP6_7Y@a8Q$iUHV(-yR{I|(f+L1ea7@p zWjdnDhRiQ>I}jQ|$8!V<@Q1wg7qa+Hz&VtVM^&L)n;1e+MS3#h;X!9(vTgW7l_dJ) zCeE_#yJN67gif1r##cnv_%;sdb?5vEn|T8P@PuJRN13j8>k<;jo9%1|XlMmR1v6`F zV{0sy6zjDCGusXU6B9C>>}X*k0U(b0X^_sx zPsQ_fz8?;YIRt_C>A!N|#{f)jY<%3@%AzkeVW7!+{dl&X{EW?tfR8R3cnS44P<}Pi z(odAs!~m>WRm=XH>!$NEA;4UbG= z67nw*Z!dWJjxXv@hYk%V7YM&4U~yVmIT0w%ukGu&Ok>vh;sb*pzT%*-(7RGedunoa zL>nGJe{2Xot6KzbqZ%$5~$8X3i)R3ne2D4nVgl?*@X*pUof|14auK;K%ob1J|oykg%_#KjG z@P(|-$r!=_n@g!l7F$OIr2O@*+9BT4CxSLP5W{2ZKcS{|O@;o;IjjU~T!*3{H#xnY zNg*+gFqSZnh>NIvT}X$zAU7>x-7hqjUJ%{0UmaH#1k9YU8loQLgNt-(-ZL*mo%_`v z-hWk?o9xo)okR9o+s)UDHm}{!0+t15E>|W+%Kn0T6r9wlDCp6QPM$HYy$CJdy#JPq zispQU@v287Tf9VKJZ`qPUyZGc0%l?#+epd1Bl9vlGTk@>Zypj9itlQaV` zd%#fy=3)FlfBpb15QkJYZ>A`oJ4Euh^UQl(d_0HksS*|zcJ2Cw84DIb-K%%MKZ#QQ z3hT3O-W<|)-4X|m4M0i;^zy5ika-07fqXwafo|~QX_#;3!ejJY-a3#;rSt$khLzm9x4Y~QqrW)<=Nql@ ze&A3FDk?(&vFWj5{rSFg&9eAqW#!`1(f}Ie2%ulCPn^g9G(=wQA!2_adRWF1BO(m1 z)IFU+RREZ)={9$gu%o9S@o|s#S-!#i7Ce5jy(|+;dfE)<8ooa%J}j5-msN5*|KWb~ zGK(FyaT7c9rchV1!g*^kNSS*6iJTZN-t_2c@dN+dZ?7Da;+6CFZ(Q6F*+NzyhzkS9 z^f)KtuZOMJAzY+BZ^P-BzHi@*Tz>U&Q--Q_hPcJ1nh_rlpV|)^R8-OwDy-y1L$j=SL2Y1=GQ*U?@2`P%*1 z*@55RL0>BVW)|-Bity?p3p43GDJ;4Y8=NlA98j5jlY^BXXKZZ7Or#*z69YZ_>)26e zIlqhni_!hdEI+I^*1&0x5-dI+=yx!mZvz1OsvC2_eDFpK*e0NCu0>Q%{C>RWWp$oo za8~wj7FBC8^NhKF7WsjdKS8(s_Qv&&uKw(z8;JkcNakxE1tYL^;t-ImtLl3H77>B$ zpJoUDCMJdqp#KZYi?fB6s!LVs0be@)?S>Z=6kG#sqTCTA~`V8ISd@u*tP>g zuvJBQfulSk0G2(TYh?nK@|@~7cY-e4OIPZHH#W4ueE5J}A%o$cM|OZDbTEk^ZAawQ zv>=bcujJ&<<-})~U@PT5bMNFh-JtHHzcZ^_{(fqvE)AE?kafr^${SeXsK?M^U@Aw?gz{NmN5lIJ z<^N7y}bb~P;KS^g3|wIPZ9GFma1 zx=T3QhU=RXBd>r^VF1EEpcW#Fvl7e>L8(0zcYsp6sMSK>7gIsy-Dr&Dz*fn- z7r6-|bK(qzCKe$o;BdIVDe{fjYfnX=`{1gT zMi<)b4DWrwJRhmzDG8&luFkGaTY(Pa$Onq3pXLbOY6xo08s^S{iLKE0N(#1r==wYAe5gh z)!tAf_}>78wRHPO*NpcgzK_sBi^08{nAF5UKq>-|Zx2FI3}!O~TfQIOb@lc3ZCX+( z^nS|aD$W$8nt(z+RbAbRj)s(+JQ&C+n*uZ?{We^-TilFHOgHYGDN{H`9?O_iQXRS~)%2S7UCJPQ49zPh?rBLB0^v2SpFcudWhT&6Le{oIxlU4Prf$;9LZ$XO339+Y_rA|wCnTV<{YFU8o>mGg;dJ> zvZP}5S>%^cPg0!{z(XGtf5!5#S{N*R{fXMRO)p~PyxTv`cK}4n4Yhmn7C0G013DZE zih=)FNu?atEtqYk1HJPV0z%ozypiMbe#gg%V>Frxh3=Kn#i9M-j1D)`1os!{;m1iQ z4R5H>qNO;OPMdwn%$8L-AAs|Ks20>J|0*6%?r|-~r5+dB_@~BFFu^kpItLL3Yc>QI zJID&L{+dhEzPZ2v9-;;UC9os-VQ~iA`B*VR*fUK)j^5^BZ+UEhYh~fs!U5PVB;ntN zQKuY0K}ABX5pm+dyjERI@cp?j5VR6<$%-=0X0Wy|)N*jlIcgzljJRq>lolIkV;M8j zD&B(SyV6=xhwQPYk{nw*C1j_^=nor(%iFh#9@hw+q2l-I_Pu+`UjQuH>EGRvmuJJ6 zYot~xt2LIgvNGSvfLXWAX4_5s_8a_{PMv9Kw#@-xG@qI)lq{{T7Oh@JktGp>00Tbu z1AiD=_Ulz)B0p;JV`okPqd|l)5XHdrWd?@j?~q^v1`JXn#OURTwAyVmfS{C2miN|R z`U@o`rC2D0M`xB9iGTkN`*~kG&<4cMim~GWeFdPBLQpTi5CbmRP!S@Ghsk6kdMt!8 zaZnMU?*#5kf1B-VA9>Upk4{`p_@3a8mk1Tn@4aP&`ipsRlxB*PDHpoBd%{|Mk{N%W zl$@lIP&4_x3c$7XhvSI8NC$-;FF5VOoW@pS8PzBKCryU;wsV*#5gMXowfcK!!z2TR zk)Gq*t{=tH3r1`eD-on0HD%ba711U_8#6{mEkePcSh5FAiW9SmjT?+bOC=vJ&WfFdhV_s10#}1t|7x4Y$kAlF_ZOSOUAZ z6{|+OhZ`LVbu>CbkF54KI&=;+QCDwXWxXSrovJ$oV%ZZS9;7U@lkz?@ta*e2vx~Gf zVMdXmxo1@Z@uvL5PSgyHr-~TPl5p^rfxGpcD{bQJka(OC>b48|VzaqS5+h+D1`CKw zk|g_4(!0e>$1@t04NoW(=tvVp_W4l_8w~--`E~^Uh*AWBB+NSy5@JIc>`oW0R|box zgfaXm2?P}Ra3t4_FVPnGqID^PED}-{|uH0uvu1^ z6zSEwv)IHX!~(T@AA%=iA~51+|L|9zJYAK@%;<%H3Ak_blH_H5zfsHU2N;QZm*lw? zCz##h$(8)S3@~cMKL|U~1;&WE zO`B(`039I)fRazO*-#4wAq-hG5)Shl{z(gGI%YZeJ1avQ`5$f7U<#$-9puU`G$U*K zyzBWJ$jsdC-ng1I;I@c41DYzux#b`#GOq+la#brypq%%cX?SQS5)A8GFVnsS1o(>` zD`l#F2*rvsQlH>=*$wx=PP~eZZa+OuG9H*TaR>+nSVF6`4xuJTHcxw7i=mpOXyg2! zwVZhGlDQa|LRQULW1g9A}uSyk33RJ=BW#TW&o{vC;f~tZ-R2PS1H=qOt z_PLErjN}v+9(**r9^J?%KJE_l9Zh77GRAvD)$tSPE32dZR@!i_gt2M#uub+{{9&zJ z)o|bRN3!y5v+Z9~RR}uL2EVXeV9q%hAxTW;IfxfDk$MLr{^}=;2}TmbeYI?<)4`ZA z>K-@xIPvH{b?1c-TxhFp&X)YularH!MvSv-OCnBA%uw(+$Ic1neSy%-tjvfgxY9tC z4-F0dDvPxZGzGD-vCJ%ND8RQB73UwL7_By1VoORWfNgU0SdnhrS&D6mO>gm;DUxUM z2%ZeMP5E@LNSo$)mnp4$MS8@^HI4|p}8A=wu(&F&(dGBhjkXCOr7D{L=f7* z$X_;HcK!RO#wr`ClY9dV@AS(~%fv&V4M4=PQk>^|dJ#z!(Ng2r1l%Kmt|j{-SmMo) z^+(91vi5J@GUxwb58-8XLkEtT1A~I?7QySk!@vkJI!|(hu4zvX`)YV@SQM8lh#Evh z3YA{xn_z@cmj36s(&RWsz`Qf)zAsH?Mdv*|Oxmq& zLarrx^F&GFV2bW%IBAH8>a`P~9{3Zuq+B#Wush+f5v7fAj50^C&czz{vhb!Pi6d!! zOu!A_Q{Qz_e(4UIE6s~MFB3>`v)`wb?&>-|g=SyZGON&zuZ}doJ_*-_e0@>0zcXCE zl{vBTX_)MVs?WpJhF}rUm`KMK-<<2)suz$Q<;fr|Eu8@acBF2iHvo!~;>qT# z05H0skjo61)aO$Cz={k4j=j9L_U4hJZ=DAVaQ&g8qsIYcvx^HW0?$7={!hh*C4d$a z^3q1_?iy5ey;uNprDwe1N}bL$;Nk}rQxT|4(`XZgFn*yYt+!grLRlY$$^9+pXl1zW%4Xu>Q``6@{AIi8u_)7 zREGg|q63rpA|7uyyFYf*uZj`a!_!IJ1>3S-e}wog(beyX=HGO-I*H>%E4;C3z~q#m zvwz`xQ*>@fVE@9+QY1vq*KulsY$5{Cw>nju;iH?}h5$0{q2|%?{Ex>Od;FI2a>rCw z7v{+u^nKe*)My-;r>%bO4tI{st)8;9wNAW_&F18PSa7l?Q#oWb$kK_y0TwuxFt1h> zGz0kjb@wmYS1~P}K|%INPvJbwXSf@29$vCjISeR^j9LH0E7`G}%^H1xobcl@xROo| z+>gP6>|tbw;G~3gZL>+l+|;eT7BEdFuGdKnSe=GCLCdf|l7d8`3~$%mrNr<~N_XPx zHhs+S{{1}cqAB8zXDwc9e}+4uerhv)6<^AO_G|Nuo6)W^y=wo)y!@>RcIoWq_BlPB z{txEOq95Tm9MF#6Z&!$aFN(7#&Ud#cw%73pSkL1P^p~rqrb1<*f$($}O!*_73=xtK zpr<2aGM|Ug@aF1y#`1}A&Cp8k^#nB@pO(ycshL**DcKm#w$f{CZhq9TtY8DQnx7X; z%CXrwE4EH`^3C2bll|_%UK9W&60x_O*)}#dqUFzy0D1!a>m<^!JRDU@&tCs;I1|&R zzy-E{zAdX|MLo?6q6SnJP+gMAg_6n0n3&>#EzPjLPOZ&}641j5M(+Viimwg`IC`&7 zH>fzA1A|O}V6*#`H0Oancs#Z zca-**Y97bmzPfTtI>0-+nZm8f#k}S;^aqX)BWAmp#QRq3>i2uD3U{TAM^O;7?Q=Z& zG6_CCFXMJNcP8#I<&`!Hh6DS9TC*GH8(!C0$zl4#UbCh)>p$0Ww$5#O87 z!qQQb(+v%m$RN<6RTXbN_djlGZoun|ZH^5ax4NO?_H)?GL1>DuX20w@(qgjip zC>b2s8ASMPa!SL#pjNmJ~CV^Q>Ip{`EPAaBj-%WV(s6_`Shgm#!Bt^W_xnrlN*pECgtdO zMT~)qiwkH7n1E?oLqkIhutDDYcCdse zmF$IvpSvW-qI&Idx3eA$f{BL+S$4LZNTB z<1`+Zr^5Y!ig`_gI4Ze$i${8Pp{s^FkoOGono|{Fl3&ZU#q5ZS5nm^-iu#g5SDk$n$eb`Jk zm$5t3?RI38$x;M)Ya)gcsD$4xD(F@ti*h3qjXg{28f%+NvC@|(*_@4AymVYxmFD!C zZ+vF)GOU4W%qiAdiP8-o#S$Na?!4+?*}Vo^E>@yFI!7 za=zOAZDtu~Ltc1u2eK^Ol5$1czjAWwlJ4Pdy2DD#_FzVTMvuZNxBL+)9&t#K$&`uu z-_vM8@)kp6h^C@gt0gt=&%pf5g5qCPSwqL@Ss%NOKe}mh%Sw`7ZbfdJf9^JLtlk;Z z+7zcpe!QgRWgt7;Ih0tJiI{^6akESQJr$+nD(tT1-`K_NWow6NWt|>OHm1>1H$LL; zh+2{PR*8Qqr9rmT_RKxrLzCWNg@Hd1myUnzS?v%*1u!@_yMrqiE&vriTBckM=(B-c z!-~4PvNkrffIW0!X(I<}y*GZXMQ-vE1uKNlWQ>+3HpEXAe8 z^|1lFs%fO{099638eof7P*nK54S+(@iMO17FrEx(?_M^O2mlExpv&vT0|Cavx8o#& zTtdS0_2y{XuIC`OZLfF$MZKF9#ouvgU8ysM0jk>m!GU~RcLv*o%}|pQT_ZH%Ky_6f z%|hRbOz3R>ZVQ!npZ!sd@SmIObOaQyHwv$xDK}D#^GTRf`jR}pd@TXi*blo6?IYVN z{6fEpmH)7yUQib17sm7GULI9$SM!J6#XU-YO}qSK6{cvszpq4u{KBhR!KN;Z*jAs4 z7@R%i&q_OPxfc&jX0vv-f5yPcIQG^6HQEA4UjgP)gstxMtPOGU;+ub(D>Ko=IrMP) z1IraH?#$6Z`k(yMD#)lXY5)h>f0*15Zccu7N!&*9TcI>5>hHuEi>m>)t+3f+(o*IS z_i!J9n!?Azyy?l7MBN3Kzgz9$@RIaPCR|*PHbjgrU}2KI<*_5Bm3uYiq5ef8zt+-E z*f;onP6%=EuxpV7)M(ov|NCzEy*t$fN>+J6PI1dmMCEvae=&>hS-3Kfwlaz{dd$a~ z$HW???lkY-jw$;RglwA*t{Ry79SC>_mKLNS=QdL;(G^D!ZhUO3pn9LtOsD_N%rlT1vH*{-Eesdv@Qza`|2yo~Tcvj@-AVZ6J zuMhRom=AN-;ucZw)jsTt0g4d{_^9cROf%4CCxtXWdD0jzq~D zw=w%@e)X8JH4ma1*8<&pLh=TL5zmrFFNyKsLLTgbD(_Phx2fwbId!RJk!~rs@uRG4 zw~GYTxEuWHqIchxW+ZEb1bnR98+e0p-rkRgvF z0o1SGzI_A6Ksj~gKZ^#vJ9aSu1rKcFomN)WUOapB-H#P5p`bgOZ?vpky-=e;2R4su z7fGe|jU`fvi;4B@-Tb}xfQE-JYG_CRV3uI*b{F6+0kz|+`7BY6rO!MRzRw8IY$2jy zikpf;iNuHApDi<)Okix-umLJqz^D`q#7TV`G2Y)m?W{4*?AMe(kal(k28MjDB$@y! zgNTU83}BTl8Wl8EhclT>A^){!FqthtEKnpc=jpZOT!yzbW7@7Ry= z66#+9#q+uIDYKY8J1RF%I8Rxh20AN_$Hno^vchFU&Nj@ zm(rp)XN_vAu5oQbG^oifLpFAQPB=o-`_8y`pAxdfw(GoVW|u18;ei?%G!UX^BHI;c zsX&)g(%7si&WNXr=usI4O~<+z0#;(7*T}Qs!&P}H7x4lwXum5EOma`XR^sMO0sn2| zSDB=sv;W7C)4z4gn-yTG0&XAqxLY@iKC7Ru%8k{n!Gx3H88fm^F?6+PfQc{#U+bml z#K_amq$kfV_wdGKjNqf%}E-t za9si-mI!h&m{b1wCDO5@b(QbhS5=dk*`mOA2IdL{O#cT23l31{XquAa`#ceHR-)IhMv#m@dGT)0{=|!hB7x3S z373_};5C4VFP7rqj4ok|Be?+`WeF)O11g(<<+I=Ai*u$7m2QWS zt|0{Jl9mz}B?Lj~7`nSlVFZRw0VM>am5^?QAp|6(n*pVfkk04ud7fu|zrTF{fbXou ztTo(q&pr3tbIv_y@ArP~JZ53x>QdZ~B|!31G38KH61j1lH?oP0h_(Py2b&sw;AxIk zFI?WAsyYQhqQKm&+;@vk%;i-m4oU9B=CEx|y&-Ra3}a-QKMpsHMS?4m^Le2l@O%KB z#JIhGXh_KJX#mjv2F!WJIn;h#(V5;p$}1mP_wWgnk(c)~X&IZELaLWO)+D!x35$;> z4KU{O3J=Z5plg<4Y&bv88@7qJq>k%WKt+>}_Sp2-VdZdfNu~AFRIRw|oG|_gK{(q*m#`?d4@A)92XYz2g4#_urySK7VN4Vp1zJZ66ig_>rUd zuog{J`&_HMlTWWF+F7pj>FQPb4`CW-Q}v&ZF`_FD=mg)g@hEjFy6Han?8tsWLml%= zUh_`VtFr*MlME||0;XtDMk-bhEt|My33=8H!H`|;ce{QfD0PoSaTfaDWq2nqwG$ja zDKE@NvYhU6^16hMn6^*mEE0^cNV4T>h5t=TuM?1FUjB$DuMHP7w~l3Jv!@C^4h%gu z3AdqfW^*$lD!I*2h*Q2Kw~ncntlE*Gn22wrYh~$TrUl{gj0y0vdDO+v;S}{cZTG9v zkow~c@#GCi%E^nOX>E-q1eJc&G#+vfOb|Yq_&FU4HWT3jcKeD>Dw{KQ)llu4P1mi7 zP#A#Y-NfrmO-1^L`FxW{}>$CDYflT?X4`pN?2 zBARnQWuq39U0i4#%~@a*2E|Qcv9#q+bD+j6#G4jN3c1vjkje0m>Urpe!U1MB&bRa_ zI4+~2!{f8C7!8G7mgnzm7vDx;mUILdk|)VkO?)@9B}0n5VR-p<7|t&MbrQqSSny%& z!+)4pe7J9Ej&;wAYm_xyP+vs8;MP+B0b1rUO!p9o)ap^ZJejAR_+k0iZPAiUDELB% zn9i;XVx23p5F*1?@$4~oeuH2NuuwQga zY%Z0JdB29 z2CY*ehSNy>wQn?YU1ev|vqvg}Gq{E3cB+DC?;lIfjx+V__d`f>xBIKlIbouA?VjPr z#i55rSyyz!T=ja_ondZEWiz3UIDbBU;>G}e>2$DSA)w?oUCB@VVs`1b1UI*}w^t|~ zNyx}h_4k+j%Mg?{R{By1Oy#_dYq_rmp)klzPA9$ebg9FGk^Xe4bb3i|L2hyC>|7=o zBt1PHSYh`|O)ewB=!olP49&dnd@^=!RO)6S2vhsdLSWIr#-|dQiCU z?DdgPqD@cHnaL}Zm8}-EIsW>^4Xg=ug;M=%u27AjT$!RTHDA_x*=DaN{PnI^2dM(} zu)R{>8(jbOlbBCwqd=&LA^@bim5a zzJI!!T?OyN2U3vT^GiUyap~i!bJ?z$0H6ez8{=`Igj+1wi^!aa6nE-P3woG^j`m0l2UI`b7^jrnu8NVYRlzsT}Wq&QmvYRqXD!sS&3FvGH=wV=Tp8G4-dWu z=uXFUjs5A#a@-9e4ci{~k?$h&w3IqrEhn(`@vQEcs$z3WSFaTdq}}$jJ@o!4V(Iy= zZvX;;GonACq^-qDaYPwZXNgslz0-a#x6_h;|5rv+gV|4zHR*Y2EMo*^UFJlV&wJZP z?|OR|J}tgxJKnv@jXNErxPGl?|FyiNP(({u7w*r5MBT?Qj=QG^UH*Flh@;ZDfV8t(m!%cE-EyhoZmqg!a z;)qP*a{W4x&VR4@`hD<0y$^5Z-kPuLo>b=K3mpoeudH4wA6)IQ5zZPO3k+&wjOz~N z?(6vyFg^ArQ+<`%d3owf(52rER01^ZV>LCRRAWC*@MjwrlETqSA~6RF&&F?C7D=MiR`HL8aCc)c>tJ2u&rq%*wgD;ewi@J{1 zA)e8QTNFavk(1)z?~{ePScQGCz;flErPzJTbjj)7b)Uxm-H>i$Z7{Np>2?eHLPZuU zr)>@xy^>#Qla3il&zkaAn0Lp%c6vdAuB1HBq`6R+4H;S^O%%GlpruF^pa^FrEE;%0 zTboFHQ6m%0!&|M!u+%~mGVpa9B~*N2N@4J59v1$Yehlj!=}?1eQDIZo5#5KZ3ddJP zo-7%7;^N}EjrMZX(MlIV!R77=*ooc0(IdeQA8MN!U?eK0c$Gu*2OUj!Gx!u8j$`Um z*a^jS-pVG*>S(eR`=1=o!o+~l@zih3Yg%$=+o&tzQ>#Yvqr!;EKOjT|+>B`ywK<&X zkIS9b`}B&oMf<&CExoc2iCz`Fqm((Dv0hV^sCAwrHjXF!@G?4@>aF0=4@qcPyRohO zbzg7qMv0k)AbsX52Ah_TSG#SGf>Y{8ZA8oGLM$-bKnmgfP7CSNs6jH8+H)mVrS?~L zpBxI-OYv2GGuPbd<2T{aM&V;(6-la$Ge@^-V!eW1o^lF2BwjY@%B=RT4OM@Qy0<%#OY;$kFI6?NTA;1kcBfw26?O_R@zzV5imuph1@ zTy$1rc^A<;M@5c*LWdCWSsWy5-kI(af}y+?2t&>>+E2NCRape;Vsm+#$V0z{=FE4N zp{(A`)iU%Q4!&)nbq7sM3|Ce(uT7ge*5UQOTyndzKii>l^%it)ZpK!77rpT!A-;Zp zps#+lQgX-+$I5dy-Fs>F&TuKY`a+0x7J9zHJ&t!`!bbR$QB!TV+htYfZI+1bESpbqSn{ss#ko64;Er+XDoCas$P9s+ z$@QL~P|&L3TX$T5l?w=6)&%Am;JgC@SOceq z{Dpz*e$U{g*6Y@owu=J_Enf9sq4|u%Xq%4D5_>_n!V(r|G51mtch_ORNOm5%E7>73 zpMt*nW3i!;U^O45xyQQj{8^Xj(NV-ING}%nSY((gKz0tryAen z_TDCT3X7F+#-dE_Hdl8M#eolMMOm4=9;{=5`gMG=vh9` z9XV+;Sb5q9&I~-@^rykN;fxs6Ixur_MV_7cDaA@j`V(Y<#m($zJ$is1&2? z*|yz*Ajj@k;*}3Ld9&K7le4na{rs-Gggs)Y=B{TLZ}AI0_`EO}tWH`v#{tSgFsnutHwfjQ{kqf{HKMkBFc(Un^@iXcgAq(@ zln8kjik^L@ql5Jo(9r!Z7Qi=HOnow_i@mtGxN@3~r(6qq64aIrPS^)PfyQ9k`xzsO zhS^PiQQP1`AR5Q(0OZ>4BBiGGRrmD4ey0+R5XuE17AX&2egW_q5G$@~X$gGm6@al0 z5p%wh1ned2*ol5n~M99^7i)jZ7;eh8Z)2Wass%G?ik8j z+`z#I)O5yyhhss`S^WHph9tK+ivQe~kdj8CRK%cHAgPQXV6_2DRoHh6`1eFMpB7^{ z9PR^RIc~t_Qc@Wxl;VxKtfWNu^9z~tEwDip!*~U(VTn(9sTZ>?*2D$n{!S`8X9bOEw25xSUha5XHV)L(o^NmYf*~7;Vf=qBxnhiXjyTP1p&9}V&p2R^02Mh$!AC<24BEH`$yEv3}TVaeW@V<|3~lzuFBvupy)yF|99J|LIQ(0b#MxO;;kRZN$$$+B0Iz`?RrrM$*W zt>g0e;9Ix~t=ECLx2rg#p?h$gxKjHNbm{DZ4c@eaiU7CR^Cf!9VFh4WrTq3VTaTCU zBBBbJGJpn^Kw&t($ei^c8zA#ZJjf8w**zb{Uh0#!g3|-Q>3si;jm*i{2Bg?Gb36+K zDo3?}>U{pcf8L%Hl_f>|jgQ!txr8V3iP(P5{!dl@8M6Op&HksN8&r59Ot>LT9%N9Z5dKl5K1T?@7!s^45^fG` zyvJ>4VuPQLg5F0OCLur~_n-S)0Y^yz;pIjda(a4tXO3Ksbb$2k#7#FP4XcOFVAGwi z{Q!fI>*)Gc X*d@_!vzSu^2Kc}pt1Fhtn}+-gntN*G literal 0 HcmV?d00001 diff --git a/scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.black_tup-legend.on_data-vbounds.norm].png b/scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.black_tup-legend.on_data-vbounds.norm].png new file mode 100644 index 0000000000000000000000000000000000000000..aad366ae7cacf89fa88575b8858c3978bd4efd97 GIT binary patch literal 39676 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 AegFUf literal 0 HcmV?d00001 diff --git a/scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.black_tup-legend.on_data-vbound.numbers].png b/scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.black_tup-legend.on_data-vbounds.numbers].png similarity index 100% rename from scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.black_tup-legend.on_data-vbound.numbers].png rename to scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.black_tup-legend.on_data-vbounds.numbers].png diff --git a/scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.black_tup-legend.on_data-vbound.percentile].png b/scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.black_tup-legend.on_data-vbounds.percentile].png similarity index 100% rename from scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.black_tup-legend.on_data-vbound.percentile].png rename to scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.black_tup-legend.on_data-vbounds.percentile].png diff --git a/scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.black_tup-legend.on_data-vbounds.vcenter].png b/scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.black_tup-legend.on_data-vbounds.vcenter].png new file mode 100644 index 0000000000000000000000000000000000000000..a18b111bf405c82da695d1e5011d78bd96647c89 GIT binary patch literal 40153 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 X*d@_!vzSu^2Kc}pt1Fhtn}+-gntN*G literal 0 HcmV?d00001 diff --git a/scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.black_tup-legend.on_right-vbounds.norm].png b/scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.black_tup-legend.on_right-vbounds.norm].png new file mode 100644 index 0000000000000000000000000000000000000000..aad366ae7cacf89fa88575b8858c3978bd4efd97 GIT binary patch literal 39676 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 AegFUf literal 0 HcmV?d00001 diff --git a/scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.black_tup-legend.on_right-vbound.numbers].png b/scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.black_tup-legend.on_right-vbounds.numbers].png similarity index 100% rename from scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.black_tup-legend.on_right-vbound.numbers].png rename to scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.black_tup-legend.on_right-vbounds.numbers].png diff --git a/scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.black_tup-legend.on_right-vbound.percentile].png b/scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.black_tup-legend.on_right-vbounds.percentile].png similarity index 100% rename from scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.black_tup-legend.on_right-vbound.percentile].png rename to scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.black_tup-legend.on_right-vbounds.percentile].png diff --git a/scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.black_tup-legend.on_right-vbounds.vcenter].png b/scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.black_tup-legend.on_right-vbounds.vcenter].png new file mode 100644 index 0000000000000000000000000000000000000000..a18b111bf405c82da695d1e5011d78bd96647c89 GIT binary patch literal 40153 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 X*d@_!vzSu^2Kc}pt1Fhtn}+-gntN*G literal 0 HcmV?d00001 diff --git a/scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.default-legend.off-vbounds.norm].png b/scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.default-legend.off-vbounds.norm].png new file mode 100644 index 0000000000000000000000000000000000000000..3696ba3d28c8381876a1181a12fd6d8502194a7b GIT binary patch literal 35595 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 YWDu8!6XHMlN63K7$IIWd5{k(B8 literal 0 HcmV?d00001 diff --git a/scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.default-legend.off-vbound.numbers].png b/scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.default-legend.off-vbounds.numbers].png similarity index 100% rename from scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.default-legend.off-vbound.numbers].png rename to scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.default-legend.off-vbounds.numbers].png diff --git a/scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.default-legend.off-vbound.percentile].png b/scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.default-legend.off-vbounds.percentile].png similarity index 100% rename from scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.default-legend.off-vbound.percentile].png rename to scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.default-legend.off-vbounds.percentile].png diff --git a/scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.default-legend.off-vbounds.vcenter].png b/scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.default-legend.off-vbounds.vcenter].png new file mode 100644 index 0000000000000000000000000000000000000000..c97398776db739b36421e3c06045ede896ed369c GIT binary patch literal 36119 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

YYVb1x4y@#=O^O7UE6~p>CZhQ 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

!QJ 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 YWDu8!6XHMlN63K7$IIWd5{k(B8 literal 0 HcmV?d00001 diff --git a/scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.default-legend.on_data-vbound.numbers].png b/scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.default-legend.on_data-vbounds.numbers].png similarity index 100% rename from scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.default-legend.on_data-vbound.numbers].png rename to scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.default-legend.on_data-vbounds.numbers].png diff --git a/scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.default-legend.on_data-vbound.percentile].png b/scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.default-legend.on_data-vbounds.percentile].png similarity index 100% rename from scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.default-legend.on_data-vbound.percentile].png rename to scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.default-legend.on_data-vbounds.percentile].png diff --git a/scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.default-legend.on_data-vbounds.vcenter].png b/scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.default-legend.on_data-vbounds.vcenter].png new file mode 100644 index 0000000000000000000000000000000000000000..c97398776db739b36421e3c06045ede896ed369c GIT binary patch literal 36119 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

YYVb1x4y@#=O^O7UE6~p>CZhQ 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

!QJ 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 YWDu8!6XHMlN63K7$IIWd5{k(B8 literal 0 HcmV?d00001 diff --git a/scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.default-legend.on_right-vbound.numbers].png b/scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.default-legend.on_right-vbounds.numbers].png similarity index 100% rename from scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.default-legend.on_right-vbound.numbers].png rename to scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.default-legend.on_right-vbounds.numbers].png diff --git a/scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.default-legend.on_right-vbound.percentile].png b/scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.default-legend.on_right-vbounds.percentile].png similarity index 100% rename from scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.default-legend.on_right-vbound.percentile].png rename to scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.default-legend.on_right-vbounds.percentile].png diff --git a/scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.default-legend.on_right-vbounds.vcenter].png b/scanpy/tests/_images/embedding-missing-values/test_missing_values_continuous[spatial-na_color.default-legend.on_right-vbounds.vcenter].png new file mode 100644 index 0000000000000000000000000000000000000000..c97398776db739b36421e3c06045ede896ed369c GIT binary patch literal 36119 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

YYVb1x4y@#=O^O7UE6~p>CZhQ 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

!QJ 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 literal 78561 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 SQWORLQdUrx{~&7|^#1_UDr@cl diff --git a/scanpy/tests/_images/master_multipanel_vcenter.png b/scanpy/tests/_images/master_multipanel_vcenter.png new file mode 100644 index 0000000000000000000000000000000000000000..e784f7ae4a88428df809e4a47ecb2f9cab64190b GIT binary patch literal 52657 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 literal 0 HcmV?d00001 diff --git a/scanpy/tests/_images/master_ranked_genes_dotplot_logfoldchange_vcenter.png b/scanpy/tests/_images/master_ranked_genes_dotplot_logfoldchange_vcenter.png new file mode 100644 index 0000000000000000000000000000000000000000..85e0f1de74d6712d0e8b68848775b851b8f9d02a GIT binary patch literal 57075 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 literal 0 HcmV?d00001 diff --git a/scanpy/tests/_images/master_ranked_genes_heatmap_swap_axes_vcenter.png b/scanpy/tests/_images/master_ranked_genes_heatmap_swap_axes_vcenter.png new file mode 100644 index 0000000000000000000000000000000000000000..1da4ac250731ccb42d98b8b25c74d6e6efc241b8 GIT binary patch literal 77301 zcmd3N^;=az*CgPR^G0c5IAnj34PqEL>b1oOzg-Z2#{AjP_3EOeiX&%>W3T zgM_9t1OzPgzduM>mH7b(NC*f?5kVD?jH66<50$>fPX8tEF|#RQc!nUVcW=nvO+qVT zl+C#vx7}K|xb>}85_)&Rll=%$5<;eg7Lqu%tz}pmxgDsH77B1`^SZt`KK0yL;8$Mt z+|A@RA0JI+6GuV%-qRb3PA!+g^-9$(p#7E0aicf%vO}^J>c68Mp&oD1{@r%;LqA8N z_;<1KM72RE@$aJdUbdGv=D$Vo^18&f1$65{C^5i=*j%U z*=s28X)F4#rDyX}RO)fVTm>4ByY=s{HtZWuqM`02ucFI#obm$CSJ~xa8C>7T5IL96 z%u7*rnPmPEwaYY~&Fy4sZT&dK6MMI}TPgp)qpx$Z);?F4bJQ;={QocC3agOf*NllC zZqwwWCJ+YzoXXFwP~0*vQkTEao7Vra856M7AMJH0WM@JLcPjHvf<`Xmbq#P_AhcsL zWarm~`d;rpY3-iBwnK3*$98r&&k=X9MFY;izI|B(lY1rpcDS0qnA=BQtffhXyAJ}W z?Dzg_U!RzqDP#f(k2xEx>7{wu{Ns+T>~Gk@c6_{1wO=Kp6b>Km$qkIQ5BKdC;g zKNY%{yzXaOJTbD#We5ZV<=92uooH8%Tr=$ty&ORKkLWK2hV%`+b6!t#m;S3guaYlE zu?)T9zy6~_0wHVGiIb;Ic>Ax9gw7dj$^RZ2C+^i>N4ht7=~!bKxc9yHf8|H1*Lp4- zeB!@&CUJt3r_yfNSXn#Ib z&#pOJF&clq(QW75eq)xQ^?9=R5{}|#cMN^JpXfasrMP^@=G&jy-k-AA_5ajZG96q& z%b-(N-|7>ek39sZ9Ob&OQu$q=85X0()<#mxHz%!Y;f@@;RlMvNv#zkyb3NO^e^e^) zyT(f2(U+MED8x-H={i8XSjf19XgqjC_Sjv@-V-npL0-8%ra##>R|wg6dHbS}ew}Ea zkTXQ*_R;;>=)odGQjja*9GZbi#HivNrab@V`saqzWFV=uMvuLr_3Mmw!xK4 zjw?PA5h-hb`TOEZ*k(NTpKK4F5&6b%=(9fEV)k!(<~kzg7W)1VhRo~9&Uw#pzdc8uOJ6#&$YKv}Q+l}LLz`@scYIHkAf*+MqK_ks9oi$RBD5`6e zm>Ryzvqc}#?t3qxr1-k%z3kF+ozTD?`wnKl6|zi#ek-bJ4kd;L4ode0CHP0+?W6og zE5qsLhm{cLQ$3!Erfl=L;JC^}7Rw2-hr`}?x|!c7;lp@gS~Gny$W?MR@w-O}xI8tt|NeHj%+ey6ahFCA9U-0FV%w--8wUmM z^P;A3Bt@dGt**|WrnI*$!IM13un^JQQkD)6mTq@A_E6m&zzkK3g7K5W?hPa3%tZQJG=G}jqpwtQj}Hh`S&pQxnRZuFaB9f{ zN$vq!VInlW#PEIhkRp}%S}e~6B`rL@Yu2Bh$V8^{Tc>_eb=J*`CEVor&R>*cS8TH24h+6cKRkyKg7))@w@hA(62LLj zs0mhyOxVUGOi^XUJ0gl>XN3S?JKO2Bw$AIwK?^e*pDSZX7W4}s;bIYlt&^ka+XCE@ zqh&HP#v2r?DxTXTT*&)5b4>OX z8uK|)3SyPX?(f|ToQlS>g$Y^Q3~C!V?cl3Jcv`-He{tmfwff3TIfm44hF4VIG&D_n z1}*%NxAn-@HO^K^$zoPvai=#gL1}h}Vxp`dZ{e#7y$BsvTtTAoEwsbk8=J~{r+!6C zdfyGRtk!L(C)Jji_XJ%Ng}P(&)QkhorXL<+#T8pD;C5mI9e!!C=v|WJsGg!A2Ab3uIeil8 z@-L#>sOyk=9+M#B>inqBLD=i7uHV}17S-sf9f#qdd9`ET3uJv^L9fnFikbXU)&4Cw z`V)!iZ0H?69m;r9)(QHUB%D%cywJ76fwKb3^jrUV?}p%^!Ymmp~R$4mdjY4 zw8n#%LXVRnY7|W?hXYo{`=f7Lc-?PEH^zD1^a=d^bm9XMu+D51in3n7WZ3(j=MCEE z4Sbl?Fe?5O4zJr0D)$?1Bch)lL}KNH$T1bC@tS_CyL!C~&Y9imBf(xrf`x7gbIPzL;n1_$6RghzPiFr`e#>&CK@on{;C`~Wfn_w3_Lcgtz!60Eu)XA+YwD&=Z zBa{-NLI*r6GImU(m>_s@#h1Mg<5Vx7z$j_UV_r_QZv96Yp2qGacZt)I6Eu} z0F;<=`9e-cO1af>K^onQZ6??A7<;AEJBbJxBCn8TCZXhNVNLavCFUwic@KrXAdO5< zP2>p`CdApP7|jkFukVXdKFiRBr5}ad?wW!D)2M?KW3{QB3sSUqXV3v3x!aCv8~0uA z9UtBRrPs>*lz@gxC`TNA+pu{BLwmaS(?1|Aijg>nIYMrZ6dU2sP@hfFe%|a({|@4B zgWC<{;6Ztn^(&Z#sgvGg1TwUD3Q{5-x9%apxHwgZxVmP3yHz ztcy)AA`>Nl+qsRbWPYh&e)%IIq{v_jw~5p9;49`kb}ze>S_pbX6r?{nV?RZIdEkZI&3i>JGxO>f?%q)@(>(c z;5>8O;2zks6&`!AK6(E7R`{s{X){5(Ni%WabA5tVa<));gq%Ohb`%)|GjB^Krvv^Y8_qE9j_ZBQ$R!tuXr$Ef-eQNo@OWy_HdS{gXb=ZVg~dQT7{0@o?qr zAyV%?XJ?dw^Xrbq+T<>PRK|+7n1`EC_Y-(%42h{pj1oRHM6ED+^O2gFLw97iWhq7A zV-V)!BCr=3T!W*34=`IP@%n}W(f&wx;eNTkMv7W{;aS_#4e2{BG+Cy&n0t{h1YQ)@ zTi77zdX?Tf9rSt*&cdBkMd_fj@_IH#*{?&N)IH6vwfXBul2(!pUB*=g5qG$QrY5X@u82rG-(}6ZDb;*50Sz^<@v*C8Ib^-6r7!Z2y#vI#(Czcf-PPb`VOhiQeEc9`fp1g?+14X zg3xj0z(dHvKK9;1Lm=RB?zL zIGAExAIO|{(PFS{n|B~gNvZtlN3z1@TT zZXGk|?CtYBzhB*jgkB*M$N6l78ND53j)RFyT|ohQD9z;kwC0M%HX7EcrRay~x;8&A z7)>vjtk3TPg#uR4|K8a^@oHtfmsMgT5gaO^LjY18xSldrla37+J+eCNE=wXqg{P5F zS%(XjoRV<37j}_)u4{R+X&BLn^EWi%nPsB3-iFt+L*0Blvc7>8GRZ`mfYfm(i+4t+ zDXA3``{vy!xJOH-eMujb`bh9@o< zyxIwl#Fwmr-C-puq9gQ2j3t>ET5qx(0{6`>pv>zy8pDy z7KpJVSW5eu)s2pD>{Cp>gHc+PdB(uc6SU3IF<1nXZ#=~aQ315M%%_&(G+%x-u&-ad z^=KLj>9WcnUv!pIVnDAgIq0lSpbv~Yki&-1YJzT|W>r?0BH3=;dOh2?!QKf;?Y63O zWAPegxeGAtUT4lSzzJ3O4BUqDv9UoF26i>%1z2wUgJXis0#TYTrym$HxJbStxyJyf zLuOnOY-n&0DoBdDG(K&<#}>y=om_y=`DBkf+krhNDJBByY4(9-z4sZ6P|-G8 z29bbLbqp%5*G(Ol?re-W11s!uOIhN{dIG#|S@^rK+2J zsx5ItB6LVtP0=%@@X`v5on_7GJK7~U>5RQ3`-Cu*SOVjx`8-cE3B0?;fVcOM&Yr-r zh!azm)4I?V<3h-LXxm|N=|_dheyl5lmoiw=X3O$gbmBV%?36~v>U^~jU0C9&1Mt!; zmjvW}&+60?o>U04`5z(JDjNgSZWw_FXw~#hCqAsaJ~rIzTAj(Mi{ZAqq9(9gg%FpA zyR?)(Vmw60V1!pELgJ5trDMFQLRDc~xF8%|1N)LOwpAf#XAA|RsH++<3{9|~+&YdtRERL!MpC7BM6VHlY4I+F+>xmd++p`}98L6cf?o6hgkReW5 znVE4LV!p^X0SqeH57Zd&watrfI)8|;V-o5D=fUSI%FgqYan|P$4s(;?TDNs5KF52X z?gjGmL6;&eXxHkFKSrAU2uA_?EkndG86`Qd?hU$Xh%0{CVVP29eZzNn8S!IMRD;PQ^!6 zq|5hL^=YyB5RWm3t@UjDM_W851Tn{({@p*3xbj?ocUz{2^Bi`)-neJV4-$W_{|jZa zO7iTYWY>1BhQ)Wzb$kL*9KcBPoZ2kNHbkQj8`!V-`ge>%9xu>fF+XIPH|Inko1@R+{S=6boPbz=LWdpl4=AJ?(R{Q#=PRti#sv%PH$i(b4 zdwORkex!d4-;rN6-n6`B+Zpk{m}p79iDCCOH&Lmv8PrfAgB-Ba#zxTQFrlBY_lfEY z%k?}we{4LPee}vB%qla}uB}b>L-*78vi0*V*X=OASN^{AxPPGj@NJ{2{P|@b{%Z^T zbJSCs>??ixD=I+={nUp~hMFh){vk&KJjeP$Jq$e&X-xMOYqY+>iMC6ly*g8_0?7M_@@%xy196tW*qnOg zNB0Y%{=QJlnVEzIKkZg*^d}MWX?6UIKN5upSnq~LJUSZ?ayVGWwveD{JEgZa%ZWHer0Lsk+zVa;e=diIzsZWJe?P%w=C|(XmM}j zYx^XT&Kk9r+I3NK;GwfeP4jnucXb0b7CP0Q-+2zq01vOjAO^jdeS zjy8~>z3c1IABV`V+q?}&PuqIYk!gDlj=jzPO00+AeO1Z!MnqU`L&@&xlSi+3`uBPcL)Tm9U`3<-JTj8az{Vy0=>CXZHOga z=}TfG<}tZ@rSmt@^ovFv9Njt9NaNeQRXytHJvCgqd$;8ulCJGWlJ0|bNVWQznE=Qi zHsDj1rXy3iu>AVG48zUm5bhSvvpQ=|CKZ2CKO+;`0xs_OE-K8fSn6u^xbb?~C0e^G z$6ej{A>C{KASQirzn}2Q0)t>275;C4svS74fO44H$_1yW0yi7SNKMh&nH(KKQ)RjH z=LW1+^P8ePGAvBa-;3L(;_;cHeTaFM`M`{CA_pr-k)3BhT)@Io&{!T{#QjX_GZE1( zACuKgk6(DPrKhv|Q8qRd`b;(=5)?2v;u0Q)Ph;x{T>h%c-0<}wD=BS)QPjlT_|WP` z--LSZR8##Cl-e*-GfcMHgrmbfBYkmo8SGSz!H5#k(a{-lRQPwnJsALb*;we#pV zs$81I`;4W6h$!m*K@c{T>>j|C#ivJe(sMbGeLp=t{vDlZXXx8S%in{ z2>53|WSaqiJ@GRMzj%!I0Y7>#bX1nVXwexRH24F=kibv}uJizofusQlLsT;vt!z(- zu37jr32U7J#IiBmc+5!Unf-3!_x~0uNM$NyoGcQP@MsBmm8lkM4T+&<>0PoK%qM6Q?r0$;^H;E-a`UwbZcrjzJR|rtsw=rh(5=CmJ zimaVyonKaAu?tkV0d9bePcu@4=0^t$Ye}u=)2JfnCgL{<#St#r#&{5)f}`zo2s3Zm z5xrbm)D-q#3fT|j${$BL=8Fzo9Bm4cfDVD1GD~eSAZ=?#-3-!|07SggvnI%HM99uf zt`+4qc8LL0dc`)-PzacvkSrkkc^&hAKb=(~ zL6n$)M(T`FAMvnd2b-=^luu?gAe+~rIJEZ>@$(ViFo*{czoh_27_i)UsvaXn^`AZr z7bI%{mJ5;!QeingJz%52lUd>uir>(ck()tZ%r`1aiuJ?s&N|jq;(VpetP&CIjDhS_ zh7&G2yivOZH?CtGodVF)hCTfz5lt$z`;J~-181dyd*`h9?vE{wOU$z%;)UW*Ld;iRN*LIP)QkBwYS^Mf78QY7GsB7OY*cV1qGwn| zAd@ZJ=(t*S<3X~!6c+}y>F`~_EI51R=zLb*)jGTCRMP$J^CjMtCLDemsdyA!8m}&2 zw_d6K{ytmv5mVl^fQ6g$JcJM!l@X7D_FJZY0yeDlfr(!ixy-PXDA3aRm|wbOOG7yx zA>pKkdC8QbQkUO!TxhTeBbkBq=c<9bDX(VmxG z0x9iMWAS}pLs{3x%e6U-GyxTbkcQgk!LL<4H3zr_!TQdRn9Ew?%a?CAP_U zR8Kz{lFTy$=9{_Qj(FFf*77d(Q_0QXt;(WT){wegA=qD1Fs{n1Uy<4GUG!U3c(&38redR-Rf}=J$EUs!ClvV-WA`6M2WKdsU zkqRQfA1F%~Oj(NiOTVxPA*cr^*GT_2OORj9A$d8nyS|zYr);a#^GVH+NXV)Kd4*=T6k#b*)d&-&2ZIFe=*znJ* zqVG>u<&QO=Oc~bZqGr$+(u93yGg1%{(8xLX%=UAu&jNJ;M}7R0O-RGIDa z>xr9b+rp$M@mgTi;{0A?Je`^HyHAl8?*dJ=`o^%;L$A_BGt7WVJL2U)kdB@amg*`2 z*H(Syn3rE0zt(#?^6rcg4EG8_vc>ld_H{$;hRg~D^#zI16?AV1(V|{!5@qTTOtmm^ z(y5|PF?so^YUmKOb+kACnh~B+yoSE1O# zOH4^nDkJibcBWA-{7fz2aI!;Lf)n$DV(B+qrG;*|xlCbBG=oJHMN|svAXZC4agQub z?;9&ZD3*JYT;VakyhGUQ<6lNEtlvhVoj}&-=I~fi|Y-y zP}$V!5#Urh>8GVr^@;x;Pu5g0b+h5wK3{Q&k&8Dv8%V zzY?rMVInH}wkM9AhLS9gjPyR#>L~{$P(}(Xxt<&oPE%`jJjI27fpfKgL=}q>FIFhC zXJW>a)n^!bb#+!IN>MUPQZl$O3wTgMzar6{rf_xI0=A=!LEWLOtd4mw&&cR;Vhe9T zdD+CgcUDw~{g}+p?n1#pExsVJwq&^5vnOt0ak+04G89@>ERVN5&vN_YJU`$L*(s$4 zKgeB{^oHtilgX4ze(=e9~O@7c&{L}Rm zK=CGr*%_8n$@Kd7)>T|ZA{+bMH;H$wu_*2&BJn-aGz4aql6}?B@9X@o5J6E3y*6T+ z*XKR+a549@-e|si-_cs;Paf+sJEsUKItIprW4#7WUa#o|_eUNGoTM^AIy9w}ywDk8 z_n+ted54x&-v3A~skutM1y97B*yoxa>$YmaIsARbE0!3%p_*#>Lx=a4Ht&Lsup61N{##UG8k&WXSIOx3>Ge9B{Y_WB8 z2~C~Ze)sKTnZxYQ4+FQ=!$g1FKL^BZKQ1oJ+6-X7uP&V#ifgkS#j_w=(3Njn83C*W zi2#u|H~IuHKI*G-nJXiJtpIRU=}ah)h^#C%I%_>mZ)|wb-IMZNV_r~~0H3UNa`J~U z2L2;_S8}Dg_Yw*Zy+7pr>K`0AY zVHgKnOE(#5x{o=!m;FVP5{M|)TE!{Uo&g}PZXim;EZ^LFEs;}4ekCyOy$>LT#lz&k z8vi|H{5n;S1|+-tUiM&}IB{y_H13UU**MtEX&I{;^CahlaoG` zpe+NmGsv_B98QwEIe7+h`9LbMJL$ZT%%!H^D=K~dn}4WJ}t{QKOL{-8ktxUg%k<4KGmJC>oCBJx#p3^zzphnX*^gl+dnvNV^w25TVt4BxMgXFbn^RGUE70{Ll2bd^NhrEf zOIqPNUye?DQgZnSMoq|d?jstgNlMNPa(hd#jz2rHUBe4?!ZlINs%WP0)5PlJ#<`Re zwCQxGryoep4LjZiw6+%$1qz8U6a2An8X+hx1O%%@^34)CM@j`#Sf*q+dyG82s&gBu z8M<;U*Wo`7JdNADMn%i8ad9Ziztd2+K8`ip`0jo&rHKs1%)K-?OovblOI2lJZ=&I4 zQ;@}8uO>%ivr)=_%s-jcaENHPVDc5a1M+W*tN`!<{u!&0D#y{g7u5;^l!f7kphe2% zB>r@6amvfHFK@=1*NOVg_Kc?}i9bEd|N8||z~mocI%W6xYeay3ZZHgfgtVt6`GeH0 z536-6G#6@$%SB|NAYJo^xF4N(*v`+Q%;dO z9@p1x!)_{8CV3FCHU3uz&OY^pnN5MRKGlVp%9m*OKJMDlAGO7Z2gt`)eAE`F1k!+u zA0YB+b%u?=V4OsG)X%qX-!9qnK9v7rkMY$2$u3&dp1*4z0h!M}J_5mPLahR zYLuux0d>`}7F|u2=ZWSgEkH?@@Bv#+_SYHqSRUZOfM_TJY2_<3=bDppB~MD6vB>R= zvCpTixKG#nS+7T5;pkVFP?Q>0j%qylsdB-rq{H8?d#k(c= zc1NngEc;&5O6!J89&1qyqr=i>NAuM_kG{T@XXx@00`%Pes!=KCB&LLR!Dd6CGsH+b zKE5T&^BXNCwX*4TmBnw1oukz+majYCj~NA8m^|ZTVq@fCiDY+HWm9HYIoNrOz2D66 z`he@O147_>0}oZJ*#~DEO)a`9?nd?2$$Cr;I(cF@GB>)XIMLB*2J7Ni*}BveiriWY zoIJBgy-b(Daei5;`wyQxqERbc%*+#wPg0>=HywA=-psGTHTL%VVMBZ2C);6@{A+;= zlNbw8+Y0NHwy{q)TFXcE1U!1TjU8f^>4#Mtq6;gl?oYN1x^2T$sPILX!!zl{VPH+W#;Ww^rE9N@$uUqWt*P~KXQ9BMjn^?9@`uTRIA@wZ92uG$TLrq zzglE@GrS#7JZ6Z^Z$ZoX8)LdW$MBLf@2!wA$)S$SE3$c|8VoF(KaI~k$B6>L)AYgM z2B_IAE}e*wW5ye-h&u)%Zq}HB{0J}{tnK=8j)9Sw+sAi-hGFhDGx3Qj7R96#dR_r%xnwC$M~ZIO|RHGSKF)i%Li_0v+CW_3ag zOF>HI;qF7^0LeX*L-LpXTU{K4)7K+a49Buxx%uH!VF!y*ApJ8WH&uR~`U?nV zr9!K|ul4hMfVwSw<*|QXE@w}_2GUbglpsU}oCJ*UVcFXwdRg!il~K#0HLBcmoR*#8 zHF~(&Zcff=$p~E8GmRd2k*9ks5?|S(rZaX$v6Wu%f}yq2%&tMYQI`^7d8J59DAvM9 zXRGJm0KD4bbHv3zEV~ONjZ=9$jv5Q$QZx8$u759Tn^6&&z@EmTY>0-$M7#C*6 zYrG#!T`Zv_IW+qrC8Jo38vV9t$--A`#3n)7=oOi9Q$<+)$cKZLa!{0iNmT#XT}mmB zPq~F7H#Q7Rk_7YEX~2cCn?OQ%R;eQP3fZf~cF6wcX;zkWeA}bebu|}wIG&(y_4S!H z_HGV4zY|jQ)6~M`NDc4Xqf+!0GZW|Bw2TLck8IYD3~9k5S0T5;rf=SnSOteOauDb%g*y*T(=s?jthF1ykf6%2%g@6VyUX!w-z0MPa zo2r#AMq%p1?YV8_Y7mkl6!#eUm6p((SWxIVsL-iLNs1T7oQXNI5hvI6Uo`J7a{`>L zlk8uzOBwQ7@8D)$TQk9Bt6_>@Fcq1#X+9;DyKx)C&FjxtOC4^ifA>I_2+QleaNHkS zibg0yu&^L&e-o3iu=M_d>YqOY)B7pTox+>cxo2nH;tG<^Odh=o_&s3&-wy}O&Hz8x zj_uKl$*{oy;=Qp~o^QRg9bmaE593I%}tFvS>kA4;8p3b#~w zyHVq2N0ivvFtRL5$c<;TQpgdQL@hk*D8jv$Rm)YDxNG83 z(x-hKIcoCx1c27uFgC}*H~lS|Ci3)aZ;AXEnLtd(TjkM%!MOl9kio`BNhMtv!(4Yq z2oM0tS3A?HlCO{E(1Kf&W`x5bpE&GeBCk0nU0KHo{YW~1Tg!--I!pdK=O%t|rE32P z1kg5#vteOKVFzVC)h!HW1AtMzle!)%bQvyWxQkh?Eye4}?)5MFyFi(B+DXijCA+z3 zC~rQG?;|Rt#`@wBH4?^$)8h6twroqg9=s5JKA2mI!^49)lNi0>eU5I`jMVz&>cZT+ zQ8iOMM{@k>4I79ugDI_n%VlHl$V#U=pskJH#5X0JxiXQ6xa7Tc-yQ5_C9EYkCf&P_ z7*qdZ4psmHm$1InG<;8md7mdZ9lp_C@Mv-EeaftW(zx{75F?I|3N%MAC=BFh{QXij zCEIJIW3=@vST4qq!7T#v88y(F^u_WSJtZ!_GtY*6H?a+OM41t-|3!W%bW7j(+Ce8N z)|>&K%U;8Y{%KnlaeBdLdf^%LKY>Ekz;dey@PYtziVBUWDSQK`s92D3Nq8d=zQd0p zt#5ki2xbFLiRB+~)&5@2ZMQns(_5MD?CTk$C^j9%O&HA2k)77y2LTZsuWogtc6y74 znJ{^>>Ak0!vUM4uh;ALj^x`vL{Yn?|uh#DE5~mWIc8v@GF>BS#d7*iG}vt1)Jm&`4ZLJ-Rd0>YvsbMI zc~3uLph2o~5XaX<{v(J(cw+uNKa*|Qu6{bcSHeE{-9`l-wI!9?!DnwYlbdn-eIXy4=)0Z>`ucGJU@%A9VyU7ZcHs z;th~$#Mo~(FuB`S77$FqY4;L#`7iJ0snDpm5`pW~-q(|?Wx3sMM8lMYfkwCFY(F*0rk@TzHdv8Oj z5kFpx>5VWQ{LZB1_VVM3@c7@RpMV8}6=Tw0kHpBIH<>-s*sx z&{3-em`H_O z1$(5i()ps^wZBzC$b>brPQq;KInIEd3x461`sH@}YBt)QURLnPjYP~A>|;RD#Hq45qZF?p{+AhL#pibeebEZOI|K*aJXZP-U$nK{S-t1HD)`8=n{Ne- zfKW%-v^3a__=K~X-^-Kq-uyuyTnIeJ)ig_-8kZ~n z0h?{T^f7f|?em6bA|=R^Pi-2@z7{YrlXbDD(b11bcGF6z_h#&XC=4o5z3yeDxuNc#cn#C87s{FO z46d0JG?Wz9CS}DIyX6~Ha^5W|wul`6zFIiF=QWHC4HXd)!mX-2pM9orJU2b1b*YJe zSvERrax>_zK5Hjf&jWZ3u=v{9p2>i)rnco8IpdNI)a73%`jJ^*{caLpCGjh+BdSxXm>J$Puz*}*{;y0h0AJB}bsiD%-pC%#B2P#fUdR-vn_Ok>D6cxbYi%wi( z^VNX`-YLA`0BSU=QulB9l;Xs^-?6Y}6#o3_;~yCYh&1$2Jrx}QogYTDhJFvRl;*u> z73Gl91)z_9q>Xm+6Hs3gvT z08-q5Cz$uIG?>|KTYlvcmw8)bWdA`bG-SpVX?0O$pw)+s?IKR&%o)>t);z{qfeaOn zE0-kpyK1^7yT!-a%XnIeIke_=%_zya^VtF(mnfbxMq>9vcm-+EWkrH5CspoS*&$QE z_SBfk&ggC1ga(m@oyJ^_#31d1qF;>~dVhvi-LTV7{Gc{>k)03gzG86l-WC7Mbf56d z$h|=4HXPuoIB`dF%J~ABtp7!il&Wga8f>$jZR(3Z4X{yiQzV!OO}ShlOei807%1>v zX~RhHllAl~Q$o_o(WZ~|%eTTX#*=l>!m-%JekCFFlD_WJgz0#M%beW!;u^ymicp7# zgwVExG<`zTlmeHdigS}Y@MkN+_SFwX#tV+i^`KixPSP6NI#a7+oCmin{MXriBokWI z(%rxTRu&%WR?}=Y?o$skL2|LFN?2F}@9Q|>Qny6Vn}cJ&Q970=^a_JHQ3Q=7j_^mn zqE7Bjt({x+siO!TgF_YWvf`s3E8FJcrQzUprid)8lf}PVaF-}q67+ehiCyqxI3STH zeOu=c;7T}qU%OWrfBw{Lkj`D2RT}q^~Yy@m7~IBax{_l1TJR5KHlB;GSw(60pnAl{DQjgC>jHvsw6v?xZi9m>A$ zN+!=#!bf5$>e9ZDZ*QC;rQ}BCyRqMqVZbX$e8b4IURc^NaIeh_xRUV22ES%a#2Lc# z@C{VXO(oHhUo}F7=!7UR6lz~f1NDoldp$uU96wtN#ZyH_X0PMRtmC;ZpP%{*Rwiefuz=WXJNo-qj5xju*r* zLC%nmo#kt~JWl6aOB!x^4z=)y&0?RoVBX8kA|l_7GUkHNhpCIW#`|xON7k+v&bB5l zs^dI{6_4k7-*28%1`huM`Da0-_Fne04MY5>tn$dE3;%^1#>ia13RC^ zl4y;Nj6*N|(JBP{aV&pf`{LAi=D_anMa&-^nzFJj;kj&xPtSX!8ckwmI5ZM};_~8@ z{;Z0E8|m$I+p#%g95Iu z`Au1Q?bOj#?Ngb!=bZ(&ten7FQvHRiCa+a!OzJRIl-!KNX@btl4EAp-h4`|hY6oH4 z(ba)GO9@5k)Wj|GTxoH^FszaE?B5st)OET%(Ux6GVg(j}45cJ)+~-1Rko;=0(h*ox zbiAC)*YR+H1u^2BPkT5>WUP!K(eX5=Bxa8`9ziE1)H~V*M_c!sDo&jUQKGVZlpQe5 zPKqf@y68N|)$x-Nc&Ro)-e=O2?|{ZMo@DoFD$I<C!N$AGN(pBxM z-*M)CEKe_LXudac=Y@(Vvu&K+$pIR{RUUrJIeY#qh|~FePbRovztO{mYA#}AW4QXc zss#$p+?ky9r5K75EBr;PJD3vVcBx){haWoX#!XS{>@vlS2T8=GPtcS9U2F%8FZmB-}}=+XTY+L4hO)e_s|r_qAB)IR~n0>z={rz z^uXIPC%3)>Ye{viR%6ar(hA^Jn&P^~!C6U-hs@o+sn#> z9X>>WzfYGfgR??IR=5BQX!1rboORfZ<5$tv;q%aeXR^2Y0k{)a!|_i6XLropqJqH= zwz4HEDYv7d_e_bS;+4K>sO@3|`>#MhAWU5Ch;pnUeXiURwCBfmyl{i%ej|FzR)VEy z>k2j@$T;i{t42HRSCK!8c9ZY$$DHHx=&k9*E8L|d>{T@WS+pTF^c3y=dK|dd* zHH=_|oN-m#ksr+&Li8WOnw))w+<}7zMMb`IVQy|S%<`L$MBFT(&i*mJH2Gq~5{9N- zud{pk5^l`{3z0*Z{uFWuuWSJ(WpNoaJA*E1;)s)8hJV96Zzt@p4I2GqCsF?;r^k~=jl`lLm zcIk-HX0!cmZONvJI?EDcvx|26yr>2I^}985+1>?px2E%CupNYX+9#!E>JfTbQ({LR z4wzGpy_f#uv|Q)&)Ww`cE%0|7bDKWpQ^->LVqXY7)FVBk99~j@{~3yk>fwS$W+w=2 zW5UP~pg2CxbN~4jS>E-g<|>vJKAbE_L^37*BF^Jtz&AtJqTqugbHRrFd;&)hzP8)h z-`hu=tm5500&+Qdh**WsZRC^tI01LNqDG zn64b3)4Zfgcs=(sa=9!u;Ahm3L1DG5a_>iLk0=*`nMPO=wCEP&UFmhb4%>l6)_psC zty+H?EI#%D=KGOp=P`3$$=Q)67aJ@)eU$6AO4c#-tqXbS6c!5HAQ1`I!t~mx@gH@4 zU0d?6T>&hd=kt=D^b1Sl1&eZ{TDTMRjN7fWP##Ble9K=^`ec!?DoPC{luSCxa;M0& zb0ogK8OM*8Y#9-^4UF6!#}73eH?K=9SnS_}9bz+q4*#8--vjN&-}Y}1f|%mepT0?7ta02Tqy9Yq-zv+!+M2M8 zII&>@IAYY;mllcPp9drR_DwaKiN14oj#!r#*&G|G7a*mNMEk5!@#w_T;w*9BI@f4~ zko@h6j}bQeEn8KCE4Y4sv@jPmLTsY+nv=mx0%QEU9}Ws_e*Uk4M@`z9xO{in65RLo z9olElwj$VgrQzuSitedG<9ogfp_8Mpp&o8^=W(aT^!_c;;lF%F#rrKIMy4+t9pAkc z%?mMO6b$W)@)>@LwsCiiQxd)2JXIosXPe*I9E!ixT+NUa)}@}qnsM3<4Yc%lHl`YT zrsh2?Z*7q7j(`l!NXr&U7U<;`j$g1ouPPs#I5S}fuXVO|p%fHP{jt^QrSy~a{fWq#0U}_x# zEA750FERXks>MwwNPRLAmi^HEv16~UGcaf0-nsNYM4fe570=toMMOpEl5fCEG)Q-YG?LO?(*32oySwWhfB(GK#RZ3R_RQ|=%2 zq~3}3xO2S+l|uRADfmr%xB!3E)*Vv6=}33A5mlp4+R$Bw*pl~R*;dx_W!j0d1Im}R zvWRUr&x}^Mi$=%rW4k}_d~@%KSTF|>st1l!{qelCM%U!f+4_2j zm@o<5j)5l#w^DJzz7sa^UT9BQhqYbm_&g~4{&cp4!33VQ(#Y0>_H|EX?Eoq_rN&m- zh{}tjHHQagn1K-$E)W?O#>~e%j045wI@ZhkezApR%`ZgmBEF@LhdILWw;fJm=Oj=$ z|BJUe>u~1&H{AEQXm(qNN#|1oFLBLfYKDT6{j0ALLW!f6U}d}#t|Dd5FGp z+G&Ysp8ES#PhI2k4Ym4xs;l8$LM55^M4^C@S8K*SjLFW4 zp0kI+vP7wk9SOfV{TqRr#)h%4f@_yBTVK#BJwacju2X4f_BOR~&+a={=2-@cp`Ubz z))1I}twukX;}j68qj!G!0YbyqbP?&AW(CtEA#~O)?CW1-p*{l*2>YyWOVjTC(Sy6Z z&OIkXUd9Lr$$_-GYZYsir=$4myMi6^=xPVDP$5g&SGRi;r8bShJx9Fi4=Krf{oPYb z@50M?4B0~#ibl`}`keo&Xf;1o8s{oQP&wHckb5F+HQU#8OyI-kp6`RUnU;6p4m4dr z!0~3ReceHaZE^K3o?ZP19-TZrw!J>=xxJ=1$cWMwsq`L{dpwwVH1Kv!%iMI*W8VUe z4sB61!MmD;{WkSFY`6HpeeSzep<|Ssk8~nG+F?Abz zT`>X=6iORKJfmUv3E;Y@6EXmE)8)(4Fj}USxnU3qS71%mY$yw5`848L+Z;${Tn1vN z5D-jec)fYIn2M(zGld<;!N!p-fnx~~J9z%7LX#HBoybwgdeNIGZlY!zNyiuY0=fxk z1+dq5-^f1Kvz^b(h-a!QMA7k_oD>(Xe0hAmV&?&$j^FBaE1w9j(A(s7;TeH8Sy^4F zd3``K`kokrD$p91#KHE*yR#WL$+#O6j){%Kceb9j_p0&F2?>a0Bbi^8WNrKnQUC#W zUv~9h!Sp`1T0qkg!m#<1y$p|9(+Y9I*(qSABSph@O zcPP68g|GLf&}S*{O0Sob_%{6^wG zk*l@Z+1N@$8X9VZBW|AD_R!OtT8O9fTM+-x-}XAS!;Zi-c5r^XzsfuK_itu%&?uhO zP8bmDV8w^@Y*L7k6!z@r*NhC5LXop?MP)yikZCi09Uk(4X40f7;r{Y{28`4hsZ@Xd zu1jWkh$6P%{`B^gug56EpE|oS$r}?+X6M*u>;pX?bs!iPz15$2gZljUxTU7TRC5PT zdh4)C&wGW1iw6p*Z9ViGK$?pNBGRH_zvjKMv4D7{h1bG!#r$SxH|@TLm06+Xac*F% zOcRR+r{lhP^_SPE^<0b4%#<^;>DTX8z~qU3tc}0aL6gJOns6WJ{wA5LLo{(tEKP94 zG@makAGZ`TMUN4c#qd6g$aX}!m(LMa1cPqJ-5CE((iDc_`Q7~F%i-!&+fs9ox>4}U z&UL4#wSb2{aoFvP1Kt}T+3A)bYuz33i2%U>&o-X#VR|$}olUsqHg7_yVS)JPpF}kW zN}~FgJJa=`EXZXm#k^7NmowFfyvy*Iv)?!lmschs517Urrva*9;&*zKOPeh%dG`=q z8U6(DP~5nXzySX97|x_dpB$5Z^VS6@(SsSxPpELdl9GriZwybKd2mjA;s)kni}L@@ zx!KL1iBjXmIP-(F6alq|E{TJgsRE-flvXtRX@Ru5V3Gh@^)avsTl_`>&v_Rh74$kX zvN2x{RUr+ThW<~6T`*!~gsJ_zX~pc&OqrLD>)`GsQGzrUk2CEXt+GV7%N_f}vid5! z^}l*CPkoSXS%b2&1O2nJ*A=vx!>Hw0?ou%(VNr!*%hWfCcCb&V7yhAC$t-GFEksiXM){lk9dQzx{>fP`MVyf z(wrP(8ypUtO`+Aanzc>a3jV**L?YkuK6Z2=3WRs9uf<5<-$yw<@^Lzd6RtC6;QJs7 zl*xuK<8X?8;&h7+tw1;*3G>I&S5dUt7p6l8-hBX_XuUWLGxJQ53^ARcy|8Ud^6c0T z+cRFMO|{3;%G2ZXb=)BXVzoatcC;?vq(S=OOWY;!O*t%|$0Ud#&N&-A_K6k6qBgvi zlL3p{Utq4ze0bs}-IU)+-5N-T$YM9H(r;$<3cv8v1q+2?OuCMwY|*>I)XR>24*qYI zFsb_~w~2x>-?70n+9_M*q zL}qkS#Yf+%QXt52%M6k*kI7zMDvE5u2zk{rU#Svg>W6^P7G1=RuEDtbeQn(&=KIC~ z*0S_iHHl#ci{`e$|JLrdPmR-5m9&~Yf&Bx(KYB0rbnDD`cu8dZUV~kbMq1JpZWjz6 zERDZMYCcd7QLN(1u;jMKoZjHgIT%|+#(-Js@d)3seS>uigSU)UEvp09&lxDe4F!QK zszMJPvGKufN4n=o*1KueVC8nN1R^a8#hhz*dM!HnORKMT4 zAqr?F>t=lD$Z%O1kmFbuB}5XWeSt8nOANC_)BBuGBec&5T3EFzScN!Zd{DLtl|{u} zGV_Tr^R#6Qpg{r>LB{UV?;u6?AL2<~ehrLjN9X6H)?f*nwFb7B=6ItadIwdMOiMjX zWY&ug2eIQBhW@5|`ntB3)WWtFsLcrqsD|??Q+vudxBu$GpbBw&5LGjHH z80a=SUcG3W+@#QRwzZg2SLRTL4N6ki$^ zqH12hIy;FE;!m%h!0U5X>^dy?^F+N5j#9aXdUUeIe3eUY6U^FGmnX2!_Gl3*=)ZTA zePB2|zJmE~&1jMJ)ZLb(ahpJKRURiF<wd7%b zH>>9}!ZOAyqOLxhwc^%+%=#AjbAV*PtFBK&$@IKwszph26386>owt_C{3ukxDGU|R zB=<2@Rmp1Q&5B@=MCwQc0tkr7nevH_h-96M$^9s;EkW|RC0>ne#J-CzdB}niblsJH zQ$88_X6=52!C|DR#I@1^d2+_*Zx9Q;_}Phi$2VHnTj7P1=J@y&<&V3FK;?a5+?YWj z&;&QI1u4q%U`NT0-ZrcdtGV%(zD?32LRK>0*VzF_7EBk5?+!hn`*|CWo>Z6!KsUhk z;N;{CUK<~eBff}kB)Q8bF1 zx4(HmWoAsj^n#@3Lb$_#`q}X2HNOcOPz7Q>1_Er+e}ml(&)K@y-jq{Hio==Xn{5+f z$G@UKBE!Dp^yErKWsSLw$2U~>eT$ZKmG+qBx!8i{ zQz#N}P-{{5;(Y8G9AB3I9`i%+Y}T~7)`7`cfiSVI;^GPA!)%f)ZC-7O={;g35b1~O zr{S(>U?Gs*t~ujd4}BOat#(5q3W@mn5Q}B3T3aj8HtyV%J0GfH=Q>Lo`1ObM6ifMA zp$b>X&T!|t-LG*o1ep_p5fCMHeJiB>Awu|lib`eJ!a5_ZX4j30M3zC}ef=4Imto(_ zSIR^^VAn7v@K_Nobo~@J6%TCnlhTIfIqiX%&+x!@2@*9o8%pwg^!?Opq4i^hHV+X1 zK?M5)BpGwvcCh`d;dwVG$@XjtWehgLuHkuA>Ox$6r*AzwtFUo03uoTgc|6{8wh-~( zWQm9u;%+zVF-Z~wp3OyqcK8s%JQA~eHbFu`S7|tFD0yL)5RR?{a0HxAi(=Pp-hWH6 z+b4M^LWmBl!C}T)7M65TZ$)&f8kju38ZwOo?i0*+!G-&jzlDociJMN$Z)8*a&L?}| zWWW9y+X$T=x_S{$JZ>l9=TVS2%;h+H9=NS+?CcGH5x21BM^QzAb3lIinoQ=w!`aLj za~+{GjCxFz%$1JPjFd*v5$cZf?a73d85wr4pMw@99)){3ruk;_GYVC)%T-bHiQ_F# zL#B6x|K@cdKGgJ7Oi!-gxghdP6-GZwA0h^RXXio-l(Km0JR_=L$?RCK zB}mW+kzcBYR>aG>yf1OYw|ggNaNW~B>Gp=Sge(~DaK#N*Rxd-sMOurP0#aLhTO*cJ z8w*j^X-Q*=4KVOWVn11*Cm`5L{JcV>OtZb2-j8#hhHZ89V)bEm%=3#ap=|DdKNfVy zjuTPJv+fzebCb!Yj~id_Syc8Ww}2>lr7aG~FGalOJ(bv$VpYDwW%2${A~te}^kuT3 zpg@pZoAY*O=a{>zYk}5dydcU-Jx}W8M*Lw*vHG{l3!|-~Mt`X~UDLSajcWz9!>%g3 z+o!`rE4Y#;jQbx%bC!!z506fOH^INKAnO(!Rtqf=SIl+xhw-;PDk%HSuNAVj$GFo? zPiwq52;7~pLF^O?aQ{SMf7Yz3C%HJjzmZm&Qu^D=df=s`JA~WW%thi>uP9}n7H8zk z;%pC`r<|b67{sP3IM>EJea<4r%ldt4&&zguyWF=oyReyUquUg*-?V=#`#~Zq(jXw&ZBkS0ylEqt{q_57O} z#G>cJl8UkMQ{wcJDW!h6P^1c_{zXKbn?WDWyP4J~vpv#sEFYH5dQr7s>1vl)9GTc| zdl@dHl>z)-(L5DTUw#BZ3X~RhzSqjU)97ast@E@!zBpHw>qY?DF21AjXQ=P4XQcd8 zE;k=fU%L>e)Lkw@or&K}>Sp!0z$0)ka=YE-vuF^b{8xL$Gc%=SxL;1+5)F=vg=wgx ziJJx>D%O8QU6ot#Tg~&Z$pfIcztGF~diRdE-LZovOtHgan~s1QN_gN^iW2L$w8) zlLVBvwjS9PhZ!E$^yo2SN@K2FBOyg%P|ILwma|}c+O^(XPD+|1vbrxZHh%GjNX0eO`(CBk%2`cM4D9pxF1$_r7p3DtMD@X(4ll}dGI2)0?ei}2R6&M zlO6HYb$v*HN27G<)js2Wb764C!`~g)?!R~j=0~sR^X~EAiDU-p!iWCr!~goKVjVH? zKpd4>wfMsuY+?)1bUF(Ak|3(6CUVFGl!YCoqfT?7N=d= zkB$FfjyD};y0FT{b%n{29uXOSi_1-F(HqLG;C-~9c%`4{@}#Fx3W=d~AN>)so1`o)TKW1uFd)|C!zPOp--eZy*9$xTy-I{Y{#=^MOV#fc-r)zW{_--cHYL`5A6Jx^-C-^wX*CB3_L z;6p*4TjCRMXI;|Q(>7>978i`yC6di8E53`1=~7&N{GMByH*;T4*9<(U`c8GnoalHO zTfHb~f}|Gv=dYAYM9cg(@* zL^bud?BkM~>|@2|2~nU{HI5HR)#|NX1%DkPzKIVBkR6_2VHfGUlf|okFS?~>tgv2$ z1`p2BIu&-WiRgiHO}j2BG0~}&H6eqdoK&^{e&c)TR6I)39F10ITSPz{BsV=YJU0Ft z8!RlP1ScB-6>WY5hZS zEuD+VGJsDMZI`;Wl|^5e_G7BRF+r~WE=rE${^xQ>GYih|+SVw6l$yjA{&lY$VA2em ze_P4b&Yi4n$zX4pZM2tu@U3g6HGbdZM!`g@DFRx&R#rAiK$H--cmK0e)vKnc-mdBt z9`4G@cG$q-A@0$M(RYK=HerMBO<6VV&)`^*KEbq3Kue1LO$MC=B>Q{*<{nc}^UM}) zu{wDT+uG%%azS4)-H-FiOTqUJ+7(?y%Fo_Z{T3zNn)5HG;=W&Zdi(wN$58F1uF!m+ zXXJh`_S2pli_)bs7_SqLt#wE(vq70s0>9&xko8Wja^?>T7&S$})6=8`Cs$jvdU~Ya z5f9da3dA)yz05xCMo4Zy*r!nD6Z6k$2WyfTS{@jIQvR{c)HuZ0)PPpsrrNU?Fhhae zB|A<^@~aHccXemT++K{&xT`JSQ2#3-4iEU8i2=Y9&>!=_9@`Z|b@EIe3=4^_IPcE= zU~nhLe$4(C6*+mjLodceWtu*Ge9VJoKmX3g{3SZKzN|-2uQT8sjA@-u->PMu()hmc zW|2gTDf&YNfd~bvz1F-q@O0nQxY%0-y&s1kvvb;XE;wE3fRqsQ9l!&S5)-ScOscMY zfts*KXQ*sHJ2TN@{}dE?Jc_~Aw{bU8T|A(8KZNKbT(@>_PuGXxN}gz0T3jw z41WFvErrYC>-gGcfYEJmwt5eq7VElRP^cs67#3`8(X0x6!&YBE^ER_5iU1RjL4ep@c$9t8Zujq~&RbBl;@)DJ1=0OR>> zdFgpsH}BOo(1l|)j@BeE5_a|Z;%N-Egz#)K=cx!XRq_|*7i}J%YpBT4KX@U7Zwje3a^6M`|uFA$| zpcHGga^wd1Jd#PLCH8CrVgu39Q2${dvH_i!nnxQ>QsrOs_~69+)TZ7TP(@!tzKe+i zI+nVo-+KT>K;g^NSI9`HC|0F5N+waL3>XLXgek@Hm;hO|mw++7>`xC9(IkJh4lSI_ngbV(9 zB?Sf&T8Memvv91$Zs+g%s*@ta-|nW!CJAspjNRb=h*jy0PEqC>9&m_X&_E(vCGK|< z_n|V~8M9pQOC^#6I@F-fcj<2#px`HKNtEV=<^CSx34R}>>A+h;^W~wysFs1vC;|CG zivDUUk;a`R;Ti4gw}oXsnQJhwM$f;-hv~H>f0qgvE;alvfG41)nKHf#e5X5; z6KbDEu-}vQ1J5%Mzp!Yo_T*2Ro*gY5dumMlC4(KYYI$<56S!i3Ll-B%wyRA3Y_S{x z4DjVA^?Y`0{bU~l{q(%l>=;1?+{&ir`a;0ij#v+W1M;pJzd89b>$yg~iE3WFxD=mN ziAg*^nV{t1#rxUZ?cWU~?pHjaV8_^_0NJCV|`R6W$*1wkpvUwo8Fo9EU5~4dx zVpTXT{xZpM7EzqT>t0o7?lkdcr6Biiaj1Toxx-#veU(7X$&rI`}HLi=HNn~~G6wvcC z9ESAEI0A|TM01&)+!%{V$Bp5oB9mm1kff{bL)Nlb!gQBlYjVf6siOP(JSlb0k&P2` zb$ciu9)8gAH`yRHKku9Q(l3mO`6sMRPeQWrI5A-`w0jF9KMHl%pNK-^huffI-o(yS z6bQ5Z7;|A$X8q1P;>>Wv#UiQM7G8zerteY`_lq2MvdWm(5L-A^{BNc+2fN(+=G_%M zO$1sbhw#D3`^d|=<%2w9QyLt-=VYta@C0XPwvAy7}yw5IcNs|)3imNYWH>VJXy`JgcsMw zZy1s*77rS3k#OY1Nj2z4|75rQL9h2q0xw-x|HMAs@ax3URcuyKQ~aut_n1uio1xwxZYkiKc5@nlvFDfJ-WeH#?+4M z9}6XpuBwI@_CCTy(+!7BO##sjI-GixI(+MVBgh>L`m;`~#EFo*H>{+-$kYjqzRCR* z`c={xJoh~m(nObsU&w);ceogOSF5H>&&ndShsF}p`;wKI)PEmI(D-p z#Vwp(DX*g2_$9l@?4k`8H;g$p7tjg+`>H$Z)14K`qyy^Q>#*V8T9Oo)Ycu>rh8^hfn(+;kH`zn>S8&?M0enLZ(nvrB>UX|VLozE`gR`|Uly~3HGrObIP45|hp+{(y3vOhR4Q>4^qGopf!n`_~yZv3BJ}N7;23Fz4T0_!jOk z`}k*o(>YxdIZ<;l_ucO`psmgg>PuR7VbSP=?GR2}fvE_Xsj9skGTF4AuH%zL%Vzk= zX5jvo1ozmL*U+VY)=?`rItEq>W?Id#Nt}C(ySeawOr%?9M<9n1rbAx8`p|gsfb&?R z>>?{obxcnn9{5hC-i5r)^@XUs?a;TYYPVwk`Rnef?fTc;mNt$1ioRseed||6Z&k8y zT!v=YhJBG@to#1D>z%5m$|@?N*Z%d4OG}XXyE?Vcny;=oeIJch+q_COD*dRG3mEOz zI$kB~SN#EDYvVfjpWR76?k6CFfsSOO7Y#Uq)IG! zfK0-9*YVLWJsnj_fMInQsB!wmgn=7}3G4S^(&ozR&7VcgtsG=3zz@=B3*xH&ZvP<- zKmrT`J_4z^{iC1J0U7{bYxj-jEs;pjp^wx42dH#_Gj=YTn=*vE%0jIw(Ko=txwHKU zHZDcK3H}kK>OU$lP6Z-E_FKFoA+{u{r$i) zP)%{}+PufkBR%;xurI(T)zFF$94CQt0+3eskAV18EJuR$Rz4I%fp9noc6I~l_xbFx zSbA*F7A+saH9=1bhx}GeRm73i@n1~1OVLflE$nz#Xrh3(zp9bb7`h=;T}T0M;Z(#= z_NoHaN>9B*FAn;!NDqH*H|F*XB-4zERJ&rSbJ+CZU?8hdIH(q+;V=*cZd3)sz7yeF z=+ck-RL{`gc2ln55mM*he zMNMIrcO8EQw@>ofUwNe?Qh?qANcFmY;b3h5(3bm}7MVqCv6f$}+Unu}JsG?OZtzo! zd`Fh2Ih`l@d1ap?R745C2veZ40AYc88z5SNZMaKtz>$E&4%I>9OB=uIht#^G4P0QmX_CD#j=s@3f?#AaRj{WBpP~_ zp?A;tmnbhgZBdyzbegLL5vc>S_u$Do4U(`UUy(s^hcSt`dxp%ej|>*WVHQk9ns(^gs(O0 zH-x!09dX@KT?-Y=&~ZR5BP+hGs7&0BM?c}{fUl<`;y*3&Q#?qE++_8GUiU_M*-2hi zmss#nU!*t>yoF4{AMet3V^QsM$Wm zkGDE?H|$`~R%!hLW_|(9+}6#6ZMLy33b4LE#TlwFd;Gzw7YG2Y`H(w;MsBX5I^PK> zyOGcKk7+9De5`>beR&W|af#Bhos?7+_GK?#)4DZQ`LFhwr{GShJUc+?z zojD)79%&Y>KZ?ZHemS?d1!P@x19Jb2)0*$e0DM|QZ*m~-C_u9L;=N?KrX1myf~u|_ ziN!Sc_<3;H3lo>=4_2niTO$y?=qfb|U;Qsh3{^C~LzqS~tr4KmB!>ROv_nH<$9q4| z20n+;#wQ^_Q2<4QK3aLFmr!>#xLO*^%&oJ!sr|DFFT#074M0>U9UTBsU>M)ZL^!ch zVhIqX6`XM?|GDTv4`gs=%T4^*&rCV+AI+EeqLb<+%qUqhsu|jSTF-40HJ)7rdJfi3?af$8uS`ci+pAc5WoViiGDIZl3v;>=0W{>_<&S z^L$1rk$h3nRJGo7SntI8pH#9+=a=7zcQ8nm7k0o+UjOnvr}(yh0FkU51!_TN=vJakx6`~P)wTvu9xYc5X<8%RPRGw*6-k|0ypE@y_-Sqy)vWLZ8lt-SCK zWU%ciDQN;e)-YGK+nvX4?=XCoM0Y&Km z+Vl5PcN^cd7MkoHM3PId(|x7{LDB|2!v+kA2a4(|jbDdMcy6+`;I-h_)@$Xi^Y*t{ zHTVd#5IR5~!eJ7zR*rJMT}y;k4pQVAry8NVX0Hp0iV75HVUJ~RdRN;_@Gk*%{9?`S z%(`HB5nDU(BX}bSTA5*ddwE4agkpBa;V{4(ObiE0pL8DS*=oVL~n`8WLJUB7;nPe$lcbuJv2 zUA?OXQ-?2$4a{FY-8SB;n}_9&y)GZ4*E(s|@6U?FlqO7C-Toh(_yh~$1ytvf2H5$6v z`w+wVKy3!yg(>4&JsKsylTEs7`yrsVE9AOFqop2CQos8_RWh7Xr-;0?d9v8uaS(UyVn9dPOyG=f!i}Ua4tUf7i#F zsGYl8zTNRITvI@-WH#UPRn<5^>M7HWW#g_P;0@WYX>O=8;F%Wghxec)aDHE|Mv--7 zu{}_}d}QIG3`Wg)tu6$`IQSA1O_U(32tI$(3dwMIj6_$~Q-rc3>DqtF|2 zNTo!U4IJDBgDua?^Q0LUcAs-mo{w0}BWsKnw^Gl1nNCDf_@G0m1iT6cU@`$@)YkaXgzRY9I` zM}WSoqiMC__}{E0y7;+X-5@1?g8%v|;CLtr6fs`4)@j=Dt=ebkE&EC2g* zP<^Pob?`!_BmqH{AQ-j2Ynw!lL7-GsjN&n+P zsSVIIfrkya#6T1gd#RqujYZKIU!D&iq-DNyd7s#!q8SV$gF7_6pF4b>V?|R1_r`BwgY@U2S1;5K?6BK3{wld(W2Z!`6-EI@lcKaJ zuih&jg^Cq1$j=BRcNoJjD!S3qN@5SrkW=W22;lDWAuT4j)-cItO7+ypF))EO4}ZI! ze!f6O=OkA!xTvcQ@Xb2)h%03)xofuj@J;tFM$XR)rG*YKczO#hlm9ru`=h9UD6w*K zUcu7FI*|wzrlqBnn#_H0`46q;D8mU>?Ks|)3#iTeKY1{qE|+vB>`SlYAlL`~k=jl> zf9D4fNq$6}MBrIYS(Ol~NftJ}T&`w%D=@Lws`97PdfjRTnd-r2ck#- zuLRb`@yqT%fwuUa48AAkT9Y9CAjbI@515`2ixl$JBL9Bhh5+7D1^|XJ)?Xl9pf)7=Ra*0#DTNpv8CsB0 z?U!olAW_OKHa)0JK_q`|;jS02k{oxJd%xC&2g0~d3WTDCLkdX@A=)$g`)>1RzXCGF0ArX zbs6+NMy9%eo-8e#33!GM|@IAx*(zXVW5IaJ`jAjtb#)kvnH-)$L^*Sg|th zcE#hAVf=qGDOiDPIFVr7p7sM@UrdK{DRsuHPl&eAM;3;AO{#C;-g*A`G&jE?N|W^G zc-#d`dW$6`4K(ctS=reP@eS0Z%W_|p7v3;#bcg!njru}9p~sC-%|$;m59rTd2}AH) zC=Mtv!~PrSzVlzlKY#ZY3KKzLC*zn7EV%`Df5kuAc*afL^Pthf#+Cu+s0C}W2B@BB z+AbKOdOpEmHINBgMo4uYD@<+r>$q_ZeN$S!9__{xP4C(AJ|YXhvV^-j8@nbpNJUf| zE|F%gJ`vL(ogKd*^1|!X|BQ?)n5>xYsfo2MLQ4i~J<%@Cb|yQ*Qy|C+>YcM>xnJ~S zSqZlK&|KtiBRBI-Gce1s&`|~ast)oN%;!wt+i>(=Sspl_XU4$Pej5=(cY6BY7DTnn zId=LxSk%Wx%DSii-ovjit8XgbgjSDFRQVf4qq;yBFlx}3ECwB4Y2sd%0}#x+cZnv;W+0KqJ~_3pUG$yN^9hx`p$=60|2pZgqrH{#OMlF^}} zoGmf?iN=q_>_EGPm6jc4tqQJ|$TDgRC$L@;f-O>+wqF;HC}qfzJsKQxoa*v^K~weZ z0nOq|n50{9i8@c{%j_T-E7vnqqp6*)`RChVJlIPqzLR+zSLodm}24Z(=vyKjc+X^8~XgQFCCKMx)pup@KTxz8o zYW6AtcLx)Qh>?s>D6fOaKVe!1Vy0lx7&F?mrU$NsvFJ0x+}#g^an27}3d_d0S=OS& z0o&+__`4IG+;hAitPbbfuVX|hNs+>*L~hT3{HVI&*=wdm+TArtBSt&u?iYdxsX)v4 zP}Tql3WJenZd};?;~6P{sk*w=hfRp%6mC;EgnDwkd<_Ayn|#s!Hnk6cKnHxae_X$j zR3|;5#xYkN9s-lDW0U7FgQSs4YNGdwyYMn_BKUraP618fFYzOVYcS*@kDL_bF(1!DAa~ z+yWJ)6`N^7yOoDIFr?*A_`7rrkU@P^@Ggm&WFNsNdv_t?v6nl-8K^k3YoHCj?@DLs z9{x47GkNx4ihH}Jov1lglrPLIVy&HO(S->&$!g2~GN_{+U$`-&N~c{hoNGM83Myz} zrCV+`Kl-^=#!S>-HLeq1=o6OZOQ1!k>eKQ$Uy2s&?yUE}l(Mh@G^UC**_hr)6E}~% z9@4jikF=YkJv+fa*s&JpZ#&_~m2YF|zvlO$;8^_v1~^Kv5(>mfPz_Whzkf?Zl>7>yZeXJL-$0M+OH^xh zWaMW5&8w+;jq&W@1?j(oaN96SqGI%i~Da*A=R@j%rV z6Y(y%b^&+R7^@FA$8EPa+fOH4P9{xFYcpjB<>kd5?HrkeNa)9qf~4eo3*`^Z9-bOg z4&)eLu;MKB9kN3o=o#Ge<15&m*5LcTfVbR3Q<~U0CV!4tot}M7%iWD%*F?ioZE2`V zvE-kZ)?<{`N!v3lXdi1^nCdJ@y^+693qT3ch?cU4URw zC1#ejfXjESqYJhd@=8il4mP#dNC|9WcQLWjW~5HzLlF3I2J0jkTf`<=6Vxf>rC+G_ zDs>fH34LGJmi80OBb#09LQ}+`&t4H}*e<*9&mX#}aW0)-jwb33A}{CfI}Y1RMqM%+ zqN>Z=4}aXgZz6myE@fdtTIZf6&w9zxf4Md=iZ_YI3`2YdVq6JS8~m zr5*>Ft@f-r-0kXOv9j@-A<9#tYLl#>m#2ySbG`AovgHC+X7Y!7aAGRgU~Hd~9;V)r zgM)N_Xg8(;OT|S?)?pFrxne7ia_ifL3D>)#mtW!$AUSQ?cbHIqlIqOtXCtxKOMc=hQ0_0dFT*1J3roj#z=^_ zcM}IM2WCr>n~M|XMaqtjZb)>_`5{4CTznJ(Iwku3!Fmp$p);1f9Y1LngnPFB%vjE; z-5<3P2oz^?4}{AQ?)|-u4JVCdip|qMTf*7UJ={{J1BziWEke%_7H`uP%T|<26lW@0 z0%cG$aME19Hb~>ZV+w$edY&__!KTV-hQA($Y=4hVR8}vJ{m%ci;pZ0=MzEQyqf2eA zb)*Hu)BK!=W`VCcF`O%N>)>wj*(4!7PkM|ZiEkv8-L@c-?UuF3yqef-b3+mn-{m`SEC7va|xsluwd#De;yB}w?`@d><ls^WoSLn#Xw zgTvVJ#d=*gX}W{`me<2LZlt@iC%0C)yP30!xNgYnuCa|C{KoTJCCqWXp!Q>&>gQIA zo;4D9q1S1)ZjSr94vx}re2a_~u3@OfR{v`C)LU2-*%%^7J7C_U^tO_8#ib{(l&n9Wx%2<)9_X^tVEe?5=Yb;u zUum@EJ@^$GGO<}8ppY-;(ZO`PH_`fUFD5f^KkL0O=|D{jACe5FDVev`7HZOYhn+jB zK%IuExf0mvDA?Q5LQi)~7?8TEt|;U!GWtLjoXh;Wq(^w$WVhC1$VR>|W5d{HJRudz z)KwKM_pQ#>D4D6C{w&mR>c9R@P-?dRLS~)f9-P|4k^+5BhWQS38)VYhaNc+&B8VPM zlF5q#6>DXk;h)k3C0|48(qz?(aN1#Nw}S8&Q)|s{vdc}BVevBmI>>DP zIJvbKkE>2K=%f_`o3Enp;LF5<|F4&wyy(wT_m79}8k31nuf0rCen~Lwk8cHiLs5h_ z#p9XxDmX59o-@y5{M6k?N@d|$ov`>>NAR_9oWKO;V*7?Z2v;BGV(dSuE-TNfuJc7$ z-`lK~sL6`Q-8T6Zp9_9n_Zum1s|Po+nkNMb#@mY7^YUxS8QEln4*!+oH=cEuvu;@S zefQx{S2DazcihiC9-aviM$)`-iVSZ4w?{b)*ANJF%l!T2k0ral-%FR{hyfc^ZMKfa zywK$-j2W<&iALTZCcxeUP^nM`&@Nq?In}2-($+skmepJsx3XM<^5B0IR$KHd4gN>_ zl(|`Am2#8ba&2tWbVm0E-~Al!#rs%hcd zANl2bXA#=A=fN4Re9N5$zWbK#E7`7m`?V+7N9B<^;f_xz0z>_o*XHZ6w8=4!kKvP> z4;I?(*hQ!c@!@D-pU{yPqeDsbCj;{lL5lGSk|c0PW<6r&UyXy zfPs%|>?!u+fAC*bho@;+f{Rr(xt$B@^9Sj$ccfqREp4f zHd?+4l}CRv+kR_|0T1I26L|3MvKVn=As2tEaVKADZxvfGp#KT%!As_voXAVM;P2k~9rWeBIP``jI5QTzf)uOa7` zWC8oV0s*Kqz7Enkc-$s)48jNG#yn#ZffIX{><&SpMd=-;bnpm{C?`y_9L-}+7(9E@ zF(e3%Pi47QKiuF=f@1q4r@$*jyE$5qxs<4J`>o|Zi{rJvyzTD;k^e1td09PspL zN>J*QH)&OG8B4bPm)fcG{(|MjruH9owh;jEC^ez1oi+^omE=uvGkI0DxEAF!BEZ56 zIn_HP9k#>u;PCU?uCdqlrSXjWEXn-6&pnJ3o?!VWmU9nF zdGKw)m1d9jCP7E}tBD2Yi>psJ4PCr-1xamR!yDzY{xHWV_>*+3Z<}D_LV>ESqyq+`n?-dHw9D&t@I`=nRyK~z^ zv8cLEIec+8IWgUTualJ8%^dan51fT%txl4ZhhtzMQ^iupIQj7x%LjeH7nxOrcIGy4<{yaIGj01{+OX-*H;1&^M=V z1YGYtIPaWz1{Wi?b?-+pX8#wUM*k;wb__4e)=)P%9Yr;KML+%9py%ARG|F$Z6a9Gb zbkp!f5m-keVf+QV%2bDjxpn8KUCkW;krdvdQGh;1yi1> z(l0?LbrC@gK3a$ELn8fZp$0i(L@s~MZM9EC!ySl3V3?Gl10_3BW8rY~wzRe-?B`?D zQ3cywwv^R0`Yl;m?rgZQS2Yev^l>46&7iY*hn_MGK1f%K^Oq|WRO0n3AVH)!+`!p8 z58|4!{|E{Jw@7`pAvYgO#7fbh;JBB%*Gmu2l$}qo(wnv+nYON31(>?z`;h8~>9LQYFik~xO6$+MdW}eE|P<4gEM*-xM_8K16h2aOlxkr za_x%OVBNx2Y4I2}Y^iL6cVQeHAaVFe5gSg!SOp3S>)Vc#{CUytEltE{PGg!T@5>Ue z4@pWw4Kl3;A7@0qLFw3!TKm}3n0NFk(#xFA$K4iHLVZC~V^%C*r|&j+HDHopy+2#? z$n8w-#s0`Ed~|wpjwCYUw*Gn}jGWODZCJ~Oc=p%Evo+uFbz)Ur*H~)IuHDj*C6gtZ zEIl)1lfo&TkryXTe00Z#0WauNxs#VPX=Yx3sfkCFf@o^38K;98hFUG}CXbhBBz}bU zV1(hw)%%ArjE@@}KpiDUXZY;-tj6ITlGUwtXO6C%>6QI(dQ z;x+48yze+MBgO{MBkjSOpt8~s*~1-@?X2Qn{X&MB!U?X&2iy8D=p}rdovAm+L^~~r zO*Pt>)&{h@cii(RGrP6}ONAW)lcSvMpGAkWi!2;{_@y`$Od5x$nlXls3eT}u7zEpo z!tspE=KQ`%z9tkbrzy((w&se2*qO+!T{q1|?5T^f$kX-f7l|RXyzSvG{(`%c@Yj*6 zAkKI9uV!tp1QEBdxscGS*C|(@lo$3XS8hJds`clUEz=NF!f`gT2;8R^je47ROqN|f zSJ;EV*>g}(i56n&YhXii;Mdb!va`}m3nEjEwzkG^sCOHjqh!8~gF{$e;)%~gW(IMp zgIY?0A(hoA4Kr#e*}|V)eySKf`Jotifda%r*((@(iJr7=+}_H}kbAN>GDS6}hhhD3 zc#8VD+3Y_w$c#19V8S%U!FSaLnPexs(Zoz-=8Am&Ti?$C7Nu~mqdCj`d10>w+5ldF z%om33g(j9wZEgeISfNK zId!6=KSfD_erUI>-)WHDbBL@meq!bvYdzyTfh@F(p2voucf3+nJNqDZ#L+%nFJs3& z_ECO5P5ZHVdNx)98$zYB?r5wDX2a)8>?Bhoeu^Lj;_+X=m~-9P=hV6A)^iHSaC232 z1S_LsKZvHZ&dCu`_XLqzU?4zysL{nb=2cTE?olu z*1I-lhREL$RgUJR@t>@c!SB1b597l}m`(cmU(YhHI5@q8*xsn7-sGqb7FCDoF8fH7 znaBX#PSgCyUW^Z4021t1bgv=2vgt z?&ef;lRsmLl?K^v+QP{PP~k{Wu(D}~m2P5C9(_~i)$e3g>+mJgTzGf%Qq6GQQN0{J zOv5u%2G69h@3Vn>wQ%nY+<-WVVhZoj9Hn|wZIId0Z(DGyoC)gNxJv2jabv*1LX1Lz z$1)>o;FGH%OJnM%;7Ihw-$*aS#1|An%^Ll#E-^ge8+ivvm91!ICVCp6Eis zK*2Qgxo5cSjTk8kFkrAMr~35(Rj8d5@Lf(q+gA zyzjnr>7BbIJg!UoB*XUFQv5v(MKg`0&Lh(dk|*Aax8P>SR~Y}4hLI~0DNk%_ElB_% zrOVOdFJ-46^kxUu|7>rAHZHVQ+YwIXPhL*&NVFE4y&H=>j>mE(besXQheCom4q<_! zk;QpbniHm)+~(X@IQ_e!{bC7h=++g4hwNvgv+TVk`F*I_y*b%p7=H@{hf_ez1KX(c zt(3hxk&d2S`wnB?M0)Eu)ymsq-&ndei%s@tfa%v>(lkD!)6jMv9WU%#b(!orTtmU@ zVk`F(;yj_D81#hlAbf*V&?2X$sJ@Sr_SS*93OLQcg%loC;Pe}$=3Wa9jv28ZEu ztCHI9bstmu7HF`USE-{VgxITVgEV3!NO&BiNHOZ;3lmvuD|#k+`u3&{5RoMdfF5FT z;Xw4YPqnR~ApzO>>&Y5J`5HqXW_ovpB!Q__MWp0rsaPK2pVe_r=_AhE{zyVGv4U@0 z=7_ap_%u|MvUr*Sk7oq6zdFGF{7HO4(pQH+%?+x2WLXY9;le973*TU@EIgJ-~RtqRAR{D<%dYb$>8l4F9JT_;enQ<_lDD7a zQ(V1wbUN17`sSBgKS9CrxnG5k%z%rMaQ5s9q~r4=-1t>65e@%VBm%FHue*uoGq98> zRb3IM#7HH$@y{t^e1Gcf25NK+LP#?0(v9G7wE4Fcd_BC)dI=({?NsTrW}WOHO`e%w zYl7uiA{D4652EB~kT#G8z{S#FI}ll$H#ao@TRm);u0UaM#^KaJNQ6P+=;wREqabjDu3zu;TW^8u9 z;Sl0&bbbWG%IWp<^NGi8=!ANGhOxBFZpA_vSaMN46Y9jZZa4cVk$8#Et!uiJ&7tuE z%p8NtkJ%9ScAZ3e-N=#aEPchxppWSLEX(rTJ=gE!7 z-eJSnUF#;M=2BbP7AlN2onCVm!=9I+zUQQS4-1vZca2y$y9+VQjf+{_^#8L9%UC`t zQOGBe$unGn!9G!DwX4bUEp;?f28XJuF4Xs+$xG)ORJ9Ew?frTj$fW zkBb8i-OBrQdx2*hHYXT2*CH7H=<|(9NSVhqu>#fCfvCZq(W&=ZJlVk;sSNcaSzG-F z`BDxo`{yuog`!|Zm(zJ0hos^rY@Tr;zH@Ik@q z&$*mB{fS?~k`+SjG0^ywq47uCU4|<}E;T2TRF#hmwB{S$Rb(e!&mf8v#$FyiI482T z4$hCb85v{xe7zH~sGS2_<(P z=*5-KLR7w;Cal0!x`!mtPu^JV@h^V}ou?7DXvmEf$Igx-PWqiXu2&G{M8#RX_1fKd z-JWu!p5P1>@{{%}KQHI&zlkh{H)r0GD2%Ob5?2bDE8cFizdK2}dtrfh;l8Wjr@0)R z$~il(FF%#iDgG&yS?PRpeqMs_ZG|?lrP%eEhbY5WhIDVrn06E|weQ`G<5cms=R9Er zPUpf@gbXQWy`PdpB*B-pBzTlAB1BxBb*^iKoFc*PQK@l-XSHHOAwp zH71L<|MfC$f+?=R(;^klT}&1==*qQ!X zRD_r$JZ&=H^T8;jP+FQO2D!*(zL-M1uQg1J$<>z+ck2;Otj-Xas(ZUPpk)ibwomv3 z%aEED3Le)AQq?R>AB@E}7x#o|Q8DDM^aDeLPhH_@>+c%0k}jb8vEd9!iIyN~Yiy1K zXwokFQdC-#Y?s-!PMiwf94l6lX~+BdGsVAp?Fw=!qVqw2=Um5`n^U(f7l_UG4) z^(kU(e6iXq_mrEW!u@|LgpWOi@rj8F8X6C0uEsB#`$){bZ?rh8e{{Nb=1ujXWU{RKMv9WnaV?qWO~t|k(VAGSsY*laYD21KJZ`TbW592Z z``l*Z*v~#BU98fTm%IIlncAcR9Hp|U(!M%fJ(hUs@@XTV(h{9M^YYSR_W(O6v$_nW zs!jBd7AN%|mwON|j7H(zrC?mOf$%a5_G9&fVRGBP}sQx(LsVyCER(^Ji@3f=sal8V9U_Ziv zx)(7-j3rr9Ju)DO`>t$ZSu~q}2p9s=eF=7&ZQgb|^M}fZ#1u^xxrRgKSY?QeKg&CrRY`P8WWFu~FRhiSigj`zl9;%M;v5kQDhc?IXfFkoihQ~`5upd-+2 zzPih`7um(3U@Gvk??qYRs5gsVzg|?nk?OD`8Y<#>7vmZWqP3LPmWb;6oG8dGn@dfH zVx2K8dF;p9BMPRHWNqWU*;K7R4J{zT6WTPuez{69;feUrQqngDZm?8i1^av|t?*^V z^qWZFo5nv>pjnyKqZe2clp)^~h(4n!py>0lti7^4^hgozsaPzWgr4V_BO)z{ocD&R z(6E1%UawMbGJJbI&TwXX(93u5(a7&7Yq`zZ;%tai^!fDQ2S9V&YpPd!xYM{&^i=J3df$TSih)G|UzXASU*vLqq^)T1coc8Zk%3`rkbpn%!u`Mx zj}LJGH0dsR#proQ^NeJSeBW>)FK@|_0GXKD?6we@nBEmKl%aTo%OgiATM|*O1dra? z#YPmFxm^GS+u-8Lf${DqT(GG0JQ|}`11$o1yhzV%PDhK3WnTU|kC%+?oNHTBvXp!f zirdDGw>rIYX!W8;Vn0Y>rb$!!9dXfo_Zi{}apKFr zA_gU-a^w)frx0e}K5YT6Fl=01G}=~9{&->d&_0Q=bTr)aqPpx@+-_)k4_QH>%5j`o?BLnNAiKPWi!r%M=74Tmue0Ca! zf@PPYHE^p9`)37Q+Zbyr3o*^)V!i9!fp_ZjyV<{g4X<#U3n8}hh&MRjH<@c71H&P~ zLUn5~(&8fDu5@hNvK_B=Q6Z#E25%pS3m6tqX5Zqg`j@4!!GAG);GMFbV zc3~P1iRzS-Ro*}KZpq4I4-`YtGEq!QDqNQO5j1r#;(YV4c;YYY@pX2==`PnD_`Xi&2ywX$6-KU! ziDpcFPtY)=>wH}!#(QrW3Tx2vmAXaA*Cv{KYKIiW&j-4yb}VN@t4DDR47+S#=Hq|5 z6{z>ElzW@-xMQe~EXf_~1VJ*mo3CyO5}RFs4GL5V^kF6V;sjf1eZ1ZMpgLrE zN^-WbERKc3v$-rolafPf2_=#8qrsTAl{VK7>e=&CW!~}ZB8hHBkQCL%k#;*6?v4^n zpZ{dAGsi@iTR3aUg3eM`Qe1z?b3%uPOLP>yF47NRzPrE=*5KLTzlBL`wzAbVNe>@p ztSTM>_3SV29v>o~hds^PISLGhNS1Fn;}gV5L9aJv)N<7B>MO^a(~x_B^(h$1oy)vX z%<6jgobuX-pE?C`RB)vk)8a3+=@+#{dt3Sg{M-J$h|yc{;g%=llG-}Y_R=J)jMxAV zp%<5Y)oF`>`1%taz((|k+e2~eotAbHn9mkkvp4o$&aOhB+n(p2uP3AkT3#_orzdY7`%fg5m6b*&sVBxxJy^(X4<_ia)&dlGBlbDhA4o+;%?xeJtpZ21L^SM9q zQ;BhiZS}G`{`uuVkDDe+mco&@p%)#hu|e4x(nDr3G2&sslB=Rh~k zJ)w@)bJ*RMuoR8XKp@Ofo8146DjrLYFuj0jG?QG+(<$2juf0}K%HC3hn)h8MWdQVU z!-1QZBWCSwSz#CU-<2gily%P$P0g0`Qc$ZkJFe4qKkuwx9DAU4Z=vXFmzE2o4u8)Pc(Pkz1+FZKOr%U#!rUHH zZ+*z(>biVduIY+LCBsxJeuz-~6#CW$bVQstwog@gT5&)i|Lvi>r|0V5Ux(?ax4621 zvtiAjdfsffeQ0(s`e`ZA3z@s=p$o|sK74( z3|~g{#~E&49RL-<45n9K!A9yZCz-gI@x9=6QTRcXlu_mR{Y)sg$@7B>5u%Z$5=&05 zG*$5Zq8J6N?4`gl2Hyh>*vhKUBfROK;9IWenCC+d{p7DbkG-B=yKeYdwuABOSvTMA zJB!Z7RvriXWtQ6Y5viC=PU#nqa#nH2c`j349D^rjWX%^d6w_*T?~@|OMy{-VR(9U? z&t0(H0x1RQzHY$B53l96qdrK_`p4ha78gW>%Yy^*V_`X-Z-UDT zy^LHYLx>SvnhZZ->bJ`HsjV=*dFl{S1pvp2B&6>Vw8h1}dcyb#2{0rQ;>cd?tiWKq zEoD{j$Af;Fq0z@G2sxqAy}Bp>FQ$-&*ecT%N=E)313|D{1Lx7N$jEQslY+8@Rs@yr ztcUA)6Mn2hb@QW8wFUas9g!KD!gmR5g0EhDqR$5^qd1%!t|$Q5puj1dCA0V~v?YEF zjShA#8J7>mgnDz7b})u_J&aT}*CYy*fMVwILe|jY2W46J36{#p62)Sq^7kPjNLtnE z(hP}t^91~qsCEy;Md(c6hy#$0RveBlhYV3l-fThcJi!;q!n_gcVKy9D1{TdIZnU0* zD1`)CKiJ(=C4eZ%`xnsv6#25_Pky#_S9H));#9&4*}DN>Y8N)bE&yNrCCl}rZ>5f)$CaB{EG&i; zcSa8zZ7;Rihp+CeZb?rmN)tLH&FEUnu4qW^jk!7(&@bAplL((ff6Wogo4dUxFJn2m zQlguFj>vK6X(e#`S>u6EmOpDBVGW1GB49ZkIwtaC=U*M_2^K%f3pcE zUCI^0uhpFE`u7iR^V%n($0}^GF=zK-lW67Fudt?=7pdlXMiL}mCnOgoG48E)X+ghY zVWAAfp!g;^y z`yt!R53e0Xl_F4p$|y5U+Lo_t&V}b2*o&oefAygnNM**%S=)q$Q+ZB7d1!S>t$8me zIDN(ke13INa}-=y;@GNA8P59`vGl;TH}!7N2196ya1l>+g;C-GB7f>aMDhW1Hl!3P zkc(r>5vfl5ux@?~4^Z{oSlBYlOFO%i1ZUf1JsMx>Ts?4}W$FkMkwz}65e3lW>;-l5 zb&uWh25V7ef2-6JEX>OM77s49Sg6j|KEQybRS<9=2Tu%uYmqSxuD7;{wdPLNF)_T@ z9$L9Re{m+fT_sxb<#_(mFP+S>zt+N9T75YCI%P`elT2HtMijz#(p2aZb0$t7RL(^j za^UXUlxWE8AKA#dt!%jmyL$47 zp&6Juk8Y^k5GkB`0STbV10Nw0&Dv<6!4;uI-Ir_?WHm>-dY=n^)SNbj8k=Xhi!xw{ zA_PKZNPAF2fg|lIz!=XYpR|e^iY)6-6u31c^%&1oO&7Oo`s|n>mD+rol(b67O9RLY z;1dI0*&XDD2uKj64A*)5YI`syMHNCQ_jTGXII+8;vgZip84MwfOL@WxU{S2lAkj8-=N%RB9fleEg_z8pq z!-nK+cA4Qp1JZWMqWZ_HSDAF+3#*2B$=GarO)zCphM3U^lL@U`no?C%kw%0P9dZ72 z92NdhVi+B?-tADW{VaFN{b@#1Nfc447^JZbdFA>JR!kXAVTz&EsfU91S}l4wxOnX8 zdQp+#vlmr$qC5D{{K;MTWE4RT@10PKZZk@XM1Pa0?cW1?OgrNvhwed!l@}iE7%1of zwAM<5FNIKFd?;)TVaZ%jyJE#blxTHoKlWcNk*6fc64P52TMH~+ji}GeF*F15zXYXU1(QGJNinl7i_Eh0=cyK! zPDF{56fFMyjp!W`??G{bLw=H%cX}TD!sg8NHa<<5`YQ7qTC0qB9Cfe=9Vwdhr$}cn*l4VKeOMTis?0+dFwU9h8$)Wrl`uC zvBXEEex}%H`&2PYx^VE^NbZ?O_b7o0XU1db#m=+S^O>c6Hr5>JTZr&?CdD+%-(}cV zX5S?MX71A=pI|=!`|18s@A9M7%i>1rkIX}Lc)?$W?4dRy?NF|ptbK+2$CmjW<*dwtym(fT?}mp<$33>6+qT>7 zzId?0E4I`TQF!-+=(T*J8FDE8@vAYDD2;oD(wP1g)YhFLY{lu4U$!%guMBypeUM@s z#EZ8YKk_ygQ|?4t@?yo~!%q0`uMbdfTMuFBUy>VyaYL4aV)?3n&ij~=Ew%ac*}8}I zSNYla#78yW8YoFD9))bOqkUR!2!(e0V#)QSrR+&VV0)HdO&?xsMAaOF=*-c5C4PPE z0BwFPQsfdC^XAmfFFii3eGQ8b--8@1-vlYGueVS+qjc=CxvH_Hia9Zo5aF#Ws2n46 zbZ~SH(^|N{MP_lJ1j4jsk7v*Izl1SFC(s;2bwtb_4+6*bL28TB1F`L1QuOszC1YcO zGW0-BWv8X}x!gW9vKb(Yk~cTTRmEUOw(g+lQX}%0t{=hH)5?qirZv&%0vMDyTR8>& zxhfz-VS3MZjBHH~Vhz`BMj&s$%KhzC#a6_jES8c9)sigV!L0FuVP!K6mpt>$+8p2HED_c>TRQ2?PZw zk)*H%olm&#eVC59hCcO+CZ-R6%N}n_7d}(?@WdE62DwbO>#^`7`G3LG(8~TOTk(yi z$FP=puxm#JgE|jvul#M1bPP4sV}jBZzW3MdYEE!|Q&8ZjA%d;W0LY!QOYl&iiOT6o zE|unh->M7{V_YCm0Vnh}PBcV0?w&w4kLmXD;2YDy9>K4bg8V7u!NFgh&oHr<-;3KYeiXA%Y2j@~(!SQrk7+^~-Ys44^x^jes4QsFNn+V_6XcVpzaDIT3{N1U zmqvClkOxU|6Z!5-Rm~fM}3j4g0N$P za1V_RBzNusXki-t4>AO(7ay&TFBr2Yg(79d6?LO=TJg&TLgguiiIZ|v#Tbi?Jk`&B z7sNQ46$gMY7J_{#3OL=5Bh!rS3qrOGGNDyE)ftV_eZPsrq}QP08~)6j#LE! zz;U#h=arjBSB3YggTE5x4W9S3WKPox7e5ode@vLc43#&S`y1BJ>1l?6F#hJ@zhq7D zsZ6o-*egth5M@CQI-6TpB6}Y5h5L7qd4gNr4ZawO=NY#bZZST+e0xG~?^5d#N73^^ zR9*XNV_AmpTf&pIiMusD6w~6(>Y8J;fEz$(w%e7`-&P-c`ZT^#x3#u8Tod<(<9hPw z-frI&A7Z_C0AJdCx*?!sf0--x_;O@ElOXRp z`N@W7kCo1HRR|%226nNj`Vq^SOJER#22k`S#^5oD1fBuh2t97UEU~}Zv+~79nZ^jF z(+nglrLQ%_RA46&^RT@S+CgTiTo>Llcafj!NIt9IO6J<)`SUU3cXsQ1POatOK+k$8 z((VIN#m>pGA*VwydX|=CSEJ9?V~8pf7bb8ABlY3|xRFvzPka>Q;jJ9`d}}cDJkS?3 zt_Q6;UJzsI84BjebN;SYH`aT!3ixaoPx5H7O7 zfew34xK!$!r>KpA#Z94#J=RC{yS1L~nfk2X_V&I-EnQw&sD}f@_2>5g!0x3|u;)va zmI(Tlo$0Vf{F!m22E{IX_&Dj0d{OnB?#EohyY2?QArLnrA7WnDW0&^MO-jV7 zfa~>}mZwILa`1MDy=Y->ivW$q!5paJ#m6e}`A+S^wZD3U&asqgMw1@FI~%S{aXQhz z!Cxv7ct9=QOrIpdP-JxV-tcjbonfIqlz;wTlYw8c#@@6C@5FVLc+x)>{C7-MhC_y# zI>=7JNk1}l=SfVCP-`zTF&{pNLF-74hQ-NU0|&@C`_injMtN5lks zP3eFvL*g$6uRpb=0~1#Us9+4p&~cdmr2-5cXZbg=^Ac%_M(PZi_tbJsOo!0p6prQ~ z(_T=QH}x=f+s8CYq$L6!3iAjJ(9*Tvz3?8}%IY!%GE-QLd_2B&3CcoJ+p69Gap|(J&&)gnEseXEyDd0SbRnRW)7KCWN5M|FNel9$7=#b`6<}frEbEU2>RW40uT~-_qR_h zPdwX&nF3CUh}IL%Kw3hA@}W1j={g`j4Q)2dnLf(Ll`}V^RNZ6e#OeegI~tDT)Wcsy zH9D1sb;8;$pr83{z&BA&YXu*@BSVr&Sft?ViBU}VW3fjZfABw{pZ%uDAoZ1CcuHUsIafv!pB(&#eR+E4Dn3tnRrA?lCm`Qmb2=z zL~2oWRVY{0%w-I%Pq=Voj$U>0lj5>ql;Lm&^k=Q(`u+Sk(dzzW1zQCCHUIudeo{f= zq<`~1aWt&LPOZ6`0{OEKcVSpNIO0-U@t}QH`677byYbrn<$=Hi-qI^AZ4=qJt}w#i zL}gKZZM(Nf{x8yi+RQE)j;x--Pz+qo(%zv7W{{E*1s$g{KRv2R@wT`p*77SE_P&^S)2|BR#|9Eb8wP#b`@~ z7$RZ8q^MM$cNwRjmeCZ@6nxJbzT1Odc{Z>(-AP9@9yltMx?#${fHW(c;yA}!eYlsa zAvUR0N<2&q-6~EE>}3g)n4^z!F0*4$GgJYHKzZ435g&Tb-do=mK%;wXI~TYr~V!LpaXJRww~mhDLUO!;r)U(q%>iY zH0!M_ZYO8A3Kt6W$ItNqfNrJY20;&=#9FsH+FU^99F1qabWvv`HDR4o$Dzye!ec!N z6SF!HpeQ!z!QNxq?YT{vPyGvJj?3%y-{KN)g_-W}+KA0C%kK@)K19A91sE<&0mNwk zlx9g8``lNu96HSzIbj6bnT0BLiwO34wrxMQQ^~;tVTD|5zqoeO zs>SkF%p(}yG$?<#cRy`=LJnWX)SHp|MvGZRrEulcBKFpce4YQ)h2CMR%M|5l8(T^= zqX%U8S*18Lxb-+JA4Uwwv-L_vTT- zw`h_i5TRJlzqH0GiGC3o-lG^3V7 zLM7&TbzWI==`b{7m5T5ea!XO*?1fOVb4%5`cH1{_3*hM*=W>4%B}HG@`FLJ9$Yj*q zv(JWOdya0Ka?*Y;sTxW!}4@l?>v zKAgck6;@0>S-Mzt$(ZCI(O&2@2Z|{<{5CO1-NHAX_u4O5_wJlt8&D+%#1h3n5w!p7 zxADvnjA{bZ6CgMvX%(30jbFhkHClS`TyOHCj1fB@=RRYwS>Y%tEqyI^+4h0?2iWqi zuZg>jb?%}*XJsR0wPYz)wqIhNshS=vtUX?4*GcF8y@xqlp`Wl#c(>VcBokC*j1od# ze6+YOdfhC@+SW{+4AHEqe2vxJeEqh^-QR6=L!UEmti*H4T7_ zEo;x_k**>Sy6ps8+#wZ-=0EZS1Ir%jeD=_LXUNr!0Gu3M0{!|On!dLa4Rj! zer3*up?&ao%lI3Rw&}0YVPZQbPUI<(7r`rtoOSZO%6s!;X7a5mR6DG?Gx7*a{l^J~ z+G6T&#p1#oU)+~Q|4>q_wg%^BJ#DfN?9xxwqFOe&JFW61>?Old>mr>m^A=9`V_rwT zd*IK~G9%KZ-`hHAf5dqAgu)$OpjFk0f3v+?(<%|nlsejf@Y5rhPYF@V_j^7Zy7QLe?LQ-zE; zNv4s4Z-^L=c!1OE18DuXisB;~Bb0iq2ev{x8GMGPT982{_t zW_xn;SuXI|{@sppMqut;d_{dy6j`R+-lx(7-*^40i*^3ns?mv%jsqYv2E-j~ zBzQ24l7Ne^Srh5KGo55$r0IC%+|wdMs-VPZe5w-9{oL4xw`MX;u^=^8iAHPiWV~D~ ztQ5cw!M$;6&UAz*va#+E;>?E^n;LJe!SV+SnYB9u6DypvuUX8_?c*?-9%9Ff0=_h2 zTS-UNe7FDI?D77ZL)|f6B>D51D{dK2^PdqZG=~dqiM&)72etn&r3+PKG4?_q) z*!5!G*32LaIjl@Ib?1m^w-f)CP2!afBbO>=7sxS3O>21zhAo`2_!DC1r()$^Pipp3 zxxy1GxJDmet!OzL75N&E|9qn6^kyg~sg3;mmpb(I+O$gtdAJ*9WH|L6d=0C+OUA_E{6W!ERJ$qeWMtol|dl{wwYcc}|c*5hsBTP-AK) zNcV7bWX=_hD#*$4tEi~(dG5QXp9bD2w%9WcL8mZrjquf6;JYHfw=^~wUScG#5r?0o zD+qz27yQ<2^~&+FF}3U5IaIxl42NkAYhPyu683fhyM->JXXJGeAOSO@vqliTVLvFuIB_ml4o0ckPwPY@sPn-(IusN zYj|}t*XeibzLFUIxxr{W2Re{Slypc;qSF!~go5+fT_&|1ce{|az;*peMfPr+CkwNXRWvy?Gx)fk4tCd6G*}2KYDy}>o5OK!gBTX62CQF-*pF3VoUByZ{GInq{7`v6Ku7oN| z_2ncQNVjHc-6c-2d{nKr-P{czl2;>lk_5liRpi&r3g+;)8c1??>O3xodig9 z^+tG%(g!+F!KR7NTNhPK)Cl8*g%MRVhp>+Nx2A2g(SzYZId$0wWnWYP)yPDpiH92? zf(~+EjWMKmvB%hpnA)yp!cpMCJ0qG8u0EEs1#K_(x+YU{3ukCH_qE5)0cMBrHT9}^ zd(T^6QzYc|(`EnE51?eU_O)q=HL0dXW2VrFwVm@Zr?c5T0K)79<${i4zh}6V*>dP8YAba@&I=T z_%ohttRqA{I~Jd$I|DB*Zw5e#cd@`A%I&4rrOlq<3e9yF&Ts{_{Fk_td37)L>g4d| z6-cK3-p1Mx9G&@I+?Vq_?mpMo%-6L{6??CKgCt$tjJR$6=gIg4gmXRVnw(G9xF7yg zRHe^;SwY2OyA{6qxf5ScOS(4a_xj5*rq^HXv^BVT4F6Q8F3EJ_p4p9w;N^@@$opLF z)awpmbh(4{d=uNMU47+{0byt(E(r5qdVj+xzQ9}wX4A%~kKFZz`oA4)FEwg+OCOcf z{AJzD`x@aYs!i%RP88eakhY4pE5Dn}dG!KL_MXkLyd$x2_2@vdSdGhp3DRLL#be9{ z8QHbFg~-1B)ndlAgMr|z94bkjchw(D^iyK6>sSK!Bx+UiMq5$uhSiAlORSiJzA|u3 zLkmSGG%7Cox}NYSPq_N)dyIM>!($D;Z-Eji`}Py4p%B4NU9EMn0@Av$#OxW=!jlj6TCl!0pfQz&UlXKB(_N$mk*Z?!p^6`m8_ zOahbl(pKI|*_Xsm392g_93byzaq<8JcU=S_#Il-0rk3b{4F!mAxCCdtdDaPB@$X0- zK9PiQrm(<>lN^{Hf!t7p3|>U)S|W74M(SQ*o>~d)UtC0ke;Sa?Sl65Pf@FLW+DwLA zhwo+9w@h>s!PIy?xZp{2Gt--}%$C6kVmtv_RGGCJEPS{)X=fd_uf855G~ZZA!cQ)< zab)wc+L6*9s|WB$P8!!`XfS#VuzBQPYyoWh7TZkh?kvyO81ac9nB=AS!EZ{a-<2Lq z`tMKvz@P8Te|^!{E@U_4nYR^ds$G_~&h#DoyaxW0X~$FhwV{2!Gb<)TyE1Qp+FLpB zMf_#MRIuA?`PyUY#bf%^! zc_ATt`n=EWNQwHsHNWCE<9PgVc`(cC$<02mGgx~9zwjNh6VCgjq>|x8TARg#a5gM# zc1%Ws+p$5v*V(1*_|m<%$3u;S!s`R|*+pk`=JfiCL`aump8HVF`YBfa)z!DCIGWr) zDfa0QyW%A~Av+s-X(z#GBwp-|4@-#q*nCl8M%dM#W=~bF$QVu47%Bejzk-#sv8Lff zP_5!6@YV2N@N#8^$i)`QN^&L%MX^6lA0jHcz2UsMBidK02@k)FC<*tq=nR(}quTcS z0V2#wG&|@mki-q7$k>A2dNllzGDE?5An<)#E-)M~KrDUAP%RX@C|G-E(-@~m(>2OO zZMob&>*PMfUOY5Mjh&cJEJ>Wpjzyk`X1@p+UuUkTCILoJTma&gYZ(1yqvnxZL6qq3LJPiKVEj;d}P)+H-h__|#exNrm=Lr=f;lItOgd9&o zR?Zd!^K99P`RY2# zcVYhXIG6Y0j6P<{D|R$XobobhzG62i&2yq7yQf!evXYY zHvjBzssm!i8pV6)_qCw(HPPHaK{R5qifq@K)0^q_eCekhrv&fPyGEp{D4}r&cRjG zI&f`6|8(eXOP5C10bwBYF zagn(@8j2QZKU+gYH~n2f{?nMbhp#W>-O-tBqix4a&7+$v3C!L>Qdh?Tbf9<28>OZ1 z{)Ys~-g>wXTZI@Y%wGB`BS?eG-#C$I=o7`V<3soRNHH_|<}> zzn7i<`{5VY1y4g3Pg?H!9rGKD(iq6SJEHCt2E_1XRQr}dSXMxMeDnaTk^UQNUTD1z z>0uqM$aV%yuoNe%u2Nlu`qJi?R7L4MR2O+GC4;9seuf1Iy6kp&IieI5r8X88<|;`0 z7ox>9y9`*|M?T2{bRzH?nQ8->YN1rG~hFH9T5iK`%X0nN_`EA*C?toV?KD}@O^mHDO)~5 zopgeywsyE2jC*+15vW-tZ=j%gT%4}w_W5Q17juypd8{wf zClZv7MVM~dPQaz^WGBq^5CBEA-1AT-aJninm0rM1{M5LH*QEa9i?R(aDDJ8chWNZFUB(~kJZ(5hh76{hQ3|3~wv0 z_c21TW*{cVaOuz03lkOEq-81rIm-s-Bzr*r2Qo@Qxd<@2z>0x8{Q(6+bF?z$w!q?m z!G1e+j0h3TeU~pqC`xak@C+oF8<@W6mNnNzRuY9tePdogNcUovE~EPsh}m~-AkDl$ z@(s#78Bqz#;P~}cC^FOv$wp|)0!*QndT?LsD}Yr2NzOQqMy^_!M^E9+5;^+ki9cKldZfF$n{*oUd! ztUMfewQ*IJtxhB6k$oAk1=1mbIuInEU@0>lanuOUab(Tbo_V8BGBh z7${NTnHY&H)?t@Ee)VdekCAv{zae>vHXBThuFEmrM1*>zu@X+rByXyetvJL~^a~`- z1|_I7S55!Z&Mu@|O;G0!5{g#eAskxB7)qxFfdJ;C^;0A!$rgvl7JM0XQw%6=A+o>< zm@<_HHy}5Z(tcpUMQYDfs13EnO~325m!jensZVdX>P@p$Kn^EmDB0QGPXO0FySvYf zB>)!t_q!a==62i+i9kl{0LvLltSsB@41B)#Caj>i7NAs}Xl>~5b$^h@0x;Co(Tls8 z3>~D5tGCP(3;d=QDqUj=!x3fZ4<~w8GEU1gpCD`<>07?aD?^~aH#Vl-i=qIum0-^) zGJ30y!^w^J--Cd?RyG#!tUKgB0wEy_UB?}$#GpRC-)kdh@-T-P8CR(~`Ta^7Evw=i zijzXVqQhKD&s0-?iY>0KSbufywj9V0lWtZYTPQEBZg+=A!sAY46Q*ls@+M=zBXa0P-_JmJ)Akkec3cC3q+#QUzcEyLo+-!QQR{1UDcADOp6Zbed13j7lNAF0 zvRnt}-bciC8|KFK7uH*k(+&Ufub`d*U{rGdljiG_lgMNVwGAgr6hl}%pgv-CzXvH)7iTlH$LQb$Z0HBsB19Eh`#CG#fpr|^^%eBVbPa;1bmL zV<2p}`r+IDv==6OI|Wr)Zuw5nAp{%L5kNBELG>YtBuByIBLQ&pEp$;Z>7BbMl5Szv z5oipM4>o7u=oX!eqLv+fTK^wK=Kvko7KPz9P1+`HW81c^#VcwO!LSnR_R9=AN_h|KI)qiaL^*8R*ImVC@3me3ykvTVPV9!^wRsn!)RjKJQBy zL;i|&(|UFN^!}j|w~fy00Kr7Z<0_loe0iadUtth3_n>_&h>oXwN_920nbmy9>v{IW zQ^M65>DYVNL%7~IydcoPzro`jLup)#VtgU7QW`b`V#bVuf)O;~BIJQ!8G?c(91%)C zJ?fPC!5C93)4;j~p#TMuW?>?WeIv`I?MOKT1;o%_FVBjOc;Nj4!WO7fOKS9@l}hIU z`EIOOC@!Vr4J9yOd#)0Qm>lh&3!6JYVPx47!%NdsN{*uzg&ggOce-Wy{Dp;W-7x{G zTL%HHp#OnX(iR>cJodp6(5#B_D={hNHXdW{;y2BRpI~#XT z-i7#`n3sJlP_>wqr@P2n7jc;!!XGS-IvfJ_-)@y2-E?QL10a7lr+rkcBrd5(^f}Xa z@ve(ys2n9Rp%GAffv$<&>^=S-WApUhv`=3R3vE-U27aa_X>%F{w9-QN(iJVOYeNcG~V=$ief@b<3he=U0eCjqVKAeKUR50>0@E!qF(U+`k;A zFQhWLQ^*o#Zn44^gcGYBb|rXM=EawIgD0h(k2rkXPTyR~Z}*;;+7 z)2{}0s_V(@_;1$zxV2fTf9$C=ehdBd@j}ylVKb_?q~_qB;N)zn5alRC$3lN7v+Hm) z!DTJ`q*xQiql7K;?~L_Yz%+mbVPGg1y%?NP6(NRwY{t*&LdXVe2aa|`_NLL{TvD=K z6?;viChZYXXFe>WNdb2a?vLW`3%kh-PCKHXmwpm(>}Zv4G-vG3$r`vl z(f-fifYn#VL-jxLJR`2%p-N4QvyT<_) zb(J~0jT-C4_0KRFf~tl!$!d9lM3H{r6@;a!#~0%#NRXRq{UzQ^>Jx5WeS{o7Hdos+ zHZnAw@tb)bRCtnyY+_xgCS>_SMO}Y*-(~>Wa|0gZBCM#2!REFJnih|F=d1lcmr3Uy zZ>!alttJDgSheUy1l6j??C)w9i<;@`mc*%IIj{dT8*YQqeOL0B$(-s26wLzQ*&j(e z=uZ0a7-u?q-UnmUF$c4&XJpSXLX}Vj79PqKv!k<}N7bLalS21F?b+xIOk}0wfhIX> z$-B?PmEpT#UN=V|&{Ei^Q0GlLQKZeGPuQBmv70Kp+6S-}t{WTFyCC|zk4qMV2?_$=4Ea0Y%;iOG@C#VD|!9I8xvJ9SuCh+Pv2a&zFoGOCr+w3>Ij+q z_~p~)lLBLXr}W$#6!SXYD5Sys;3p1G2n2NNjV9e(Kl7)0g2zgpn3s`Xy>XwYTRYSJ zsPKD93I2SIaUX&YSKe(QyBG^3~mTnEO`?k^1Oq zgEsQw%`o#MF11Z0?64<(3S3~(K3!R-^oJ+eyy#bh6`=63C_@FFoX^8Mt5d=6N`(jxq(o=ykcyXgt;U zFn@|!EW@K>$>V zW}K?w1oOTO3~V&#{&$_}nO6}!-5$p1aD2AmZM?)63@3!wcFD>$Q}&0=weLN}Fp4smbh<^*`+fetFG!nUYdHBiV*M$<=hOB~#LRb?v!MMo>bpFc+U!^$T2-Lz9z zoEe|ssuqqWM96q}A%H{36lqG$_@0D?Xu|w|0FW)}?k&I#-x+I@4H0{_bYz&AMuQiE zM?8+nci^<3mln|u@Pm{VjzVu6Shu$qlYsMCH`{%pYXd-A^2Rfa&67gFEBqkL;1$D; zwWg5nSG+HqJ0;%AMxCjDPl&Bx(2o;pFZK&Gn}vUpNqOpi)+LUVA=m~rEA|ia^QZP8 z6kS~1EdZ^h!Oc4Bx9lFG#3RWkV)do{QT`O>+mC9k1HyoY@?DbVx0TcM@z*WbUpgD$ zccCq%!cxZ8)(6p#zS1yx>%9@!V`my(0>GyPo*rN3p#=aVP~|W?{$})-LC7o#5(tGx zFtN>c6k_!M`AZ2vQE@P;VXR<6v4lr6kUf&MrrX$x@oVSvo>KUtG-Ixz@I;3RUv(@dXYo3}434_A{OdAbWhH z>u5)LFg&~b)4kSKk}7x^R>ZEqXTiy>f0_9ftt88InW;@}19#^|o1qc9GS5r)=WHX(ukfu5edk9;`zL5JGlncx);-I^2tSH0dx%cosPkwp{z3Lw_DcM3}Q1w~4V zlDxQl3KGhso)Iv$p&opj3;HXHmN0hqVW@!G74!`E62w#F4gM|Wu!1WDONT}v6{-7* z49*y%doB)usy@(W`18MK$69RgXAVLRUXhVl@J6W7RuOvv)v2^_8DGXnFG1Ad= zN0!!f8QT}HfzLQbEmn@1fn3Po?Ez^J@88#*?G{)G(U6g+W$c%;V5axruK(EO2%4!t z^*TykUDdSI7h?6|Lyvit>HY($vUxje%W<6v&Tg9ru8_vE@8$5xrV~uB@CKDH49Ee7 z)@jzy<2(HEd1&5jK$fup&KG$aUnt)`#p?U6^1DZjvs6{j zcxDsEkMZdFkOAEfa-{1HBToc96$%jASAkVXyKzNAGtKF>9t6(P7je&#x}J<(sH zin^TLj*1a0Xxd=(C*$o1m8J89x~dWOVz44)bFqDo?|Z^EJD1*1ZjD~KDn=YbnBuRc zOEj|e07!)PB~`zYV)K&a^o({YCLVtN(`%673H-m%q4{==yLYzq<6`}ag4>F^GAUz( z57Jj! zFwJ{l5S2g8U`gnyh^sICGxQjMNc}k`Pzhgkk~|?9Zv0T@yWLb~GY}KpkM}uaM)t$@ zmk3I*UXGnk((&Sbc(M~}$Q09JUO&Q91l>f?Cr(lkGnYwGt6E8M$9)gFIyD10%W#pP zACxnMf*tRoo2p?{IEinnoMzj}{PI9$tr4s?3u+-cKKro2nfBhn{|f zyy0qcW+Xw8Ba96AU5eKJcQ?^f8Ur}WRF9#W(#G+%FwoI~l@42A>Q;yZ2|cDfZwQ4j zQOd;YH`qYH@cF6Qd~xYAGAZ^<@btOxHZX2oLkgXA9#Z3giVX1CkGf=O!ILwTfgS_n z!-@F`X@XR!6eTCE#YN!#^-`z9{0+!jBrgKD_Y-m}L;0Egl%nAy$bel^tnklo)D`ql z5_0BJ1Y+*SmP zUq0|{>U6%JS!DQ($V<+!XZ>{uzSAU4?uNw_B+SgBbz_7BHUD6+3?EUgbKrC`tFQ=4q+YmHd3sCcJMSFy z?aK&2K9JL&Q#ku9XQsqJDLPVE@eb+I$?6!^KxtSlN8U)uNno~EK@7^nD>op+=NhQ< zJb*#I4hCb&jWMrf(YKOsJU?a|Hq^5Sj9D%B?)?3_0Jmq4e^RPT*e(%Fhdq$0l673^ zs^@O3@=bdDFTT!e*|nzi{aG#meR{fv=xfp~5~gW6KKywS~KeCe<$q(!7p=iDP{i>@IZ568<8(|CpMm z*P@_o@o}j)UY&*5a7$e?MqZjdHM^cp=$G(!Kp!q`_eM^gOXnp_i)y$7C6<`N2m}xc zN~G`u7aGFz(kZkMTbD>ei0LkDKYPoxRQp84RTmY*8YfzWA=*W~E@60@^hB+zJ_cK= z(K*(RA#A^rVvrIh2}w+~*L04Q?m2FYK9SqYuqy`Bu*{gL?#yYJeBG7ix}4Nb+r0*< z4nso7$^?2&T7&}2a!A@(<8uOnZO4aJRmW>AUlGnry;GK`zlo~j!jqWf_s^P#g}C9@ zF9*nYS~4;_(5KDk-*6W%a36_e$uL~1m~2dv8zb`PB^L779A{>RDE;P$K7&btwVt)>ParLzCR=1W{2sGiLrSPNB(w6{Vj(bg)RKg z%V}+L;Y9D%pS0pS2mjAzlyE(>Ex~I=8)5y0T}-w9?VuA*=Lv64807ib{A~4jKY}c| zGJcGus!Arf^)^k6`1Rw%aqYzG0%tTfl8 z{=Sn>+jqb7)afVF&DXX@+eb6*U&=+mtZkx#n^Wvol`dAuXnqZY0f?wz!oZXpqAPua zF2_@_S6nDK{2+JF`?JQ|AfMWBUsf~Z>Crczhf~kBzcA5ag?5j5DV{RE@tOR@GrBQC zFt-02sJoGVA;~1I!99RllkU-}UbxtnDY<*C2JnX(TQ+7P#=+!CC~cfn@}1NiRUi?1!Q^ifz?Fy;5bh8M z$ldRZP)IQ0fJ_=IR=~Jy!t?qW)R^a%Jd_yyz-k~Yf&eQ@DONl@EpLO$vIVY~C~S4C zA^gZFL-Dz=5H<_^dq_=g0%Fb&^P9%xs=kmqpB8X0-c5J3|Mv0WDA#@Y^R1|Z-{^?a zrQ??6l?^w4J8}fw{uDX-vyihoHgj2g==Q)S3>lynHS0f0W9!%m3|8`czbKMRb0YR* zgBs;8lq>qg<#P$=&+}UO>JL_Je`Qx6|MdlC=;X?LauDcls+Sxp9p4JtQ_U+)kpfn$ z;WGjonjBgEzTdIaud^ShFt>dAT%~0?s%3< z=Zd`N?Zbw)#vVW&5q`qsGt2nilss~*9w|>@cs?0w0OM|2g*yW=4pEdoQiN8c{3(0#5#4Z%o$! zP?)|+%o-7HMkws3Nj9QN&{T3RXuKOue}TF%tT7l;jK)(EW-| z4}?w+eLah?y2z&FeoV-DY?L;dLP!*#{{kvAVg{+m;{HMHa>#f5!Eu zlmJPa-(-sOvEYZ(0HFc}`OQL{+1dq2hh8**I*vLe-vp0&{r1)cl|~~Tdg#qqx*ROn zgLkbS7#duQ@TMNsF(jUFR-zY@*PfREK5f5_vS2?R;rga%+xB3S8VmtT5 zC(t?9(1_cbboctoyupl(KJKfVs%&E-u0Bh01@CmcF-=ip7ccf-@eOL*-EX zR0;56`DQ$p^Y0&0)lCfar8BZul+yTe}%ePq!NK`S(vE`;xbi!J;WFUx%i0V1kV6X<7#(0 zhvO@S&Ku7Ep&Ld_xxQ8oLcNw?n(8y7OAr!3#Mb`q8%rr*KP5$h!#c~q$Rd@8uTvcc9Nm*{spbl<(pc~DW6QZUd0M=X;Y0SVL)Y7q3=xcDbILu z>Q9E)QGmW__wi`g2ZjegzvwAD_Uf$t1nT|X z*MhLQ*#iZ1ZvcU{k|@&>00Ka52^gjT&ZgS^V}sKJpgfQlY{B#w%G29Qej35Vk)mq+ z|BWlQ{>*XF7I`X}0wY86NwH}B8nTz6)U2Ag&i z91m_Lt9FiO+>H|M9fyV3=t}DKL#_IZDv9uTk~|Kq_fyKApTfck?`4Jf%-vxxJW@oK z?Bzu~fo`qd%Yd!}%)jX5b^32dO+q~oSzlj5# zTOxN&KGVqBFX~O{;#a&RPA`si@@<62s{F?uWS%Dahv~mo`Hc;;eS7Y8agy{G5+mA)dIbR+6@$?Thp_ znl^HoCb;9k-z(s~b_MpsFP2F}0HO$2AqRAJ{Im&G`OQ&wgndn3L?>|TLf%n$x6%O5T*SVA$HTjxF2Akk%G zKls*r-_g}Y3owa?eZOp_D*T`yJX9K+AM6_gR4U#6wpXuxx6}>M%J3&Og|F8d&gJp8 zIz#og0liBA%m_6vODLE~5cZj~DhuWH^7GxxmXsrbzq##3vX zWB&?EdZIOk)nfw2o5ajwOSsLWAcLRB%yQtPoFslNv&CdVA zwSv-k{s@-7;?E>J7p~;a_Ov-^T3T&P17S-%JX= z8q86M5i1hMQh-tc(_QD%vap|g-#^qT2OltYBu-@9vP`qM?g(^aCCD@RX~p)>1FK64 z!bj)jV+wXphvDm&H2zn~`FgSeB$Dp&zKM6*Pk9c`j^(AEK(DDEj)B@gnUJ@^)dLrb zv1ocY$vGoFO<9ZFY7?a8AT$Mfmraz0uV?j3A1gAo2H{$9!iT2_k|fyKMY*Gz?X-A7 z;u4ZJvD>QMu0_v^CrUruoOe}R7k1de=G7brs@(C6??Ky`nWj>_&m{5K*>SVU>zxT) zz5UpZAn<@%VR8VhjH(Q!k@GTyms3X!^aCj@j)9?2WF~Z|j)__>1}{z*xqLHfN@HB= z(S%8v3HpXocwP@4*$&0p~xp%c5^&xRH+T3#ePJ zo&GtlpK2JGE_h#Q^J;d`bG@%KeYqv+JX7afp94TcM9Tax#sJLl%gr^4Z0_TDC;dsm z3Ww2%M8jBfhQ4KzLUB@@>#OnTdP8Ur9kys~4g5sR0%G%-!l4Jm?7A0kwdyIc+e^8}-RO6vmn=&Pwp`xX^NvxG!9kFNHyoFQVrPnc?7$fF}GboXsn zmx6kxIJO_TpIWRok*wjk+!xz_5e9;Oeii)KePnQoR?&($)`akqm;e6JhTG$T5*PVK zbJ9*fRPx+Y4>C@O!LL~#38K!0tj9Of7kyIrR)L{p113+L_aGALM@8poAWe!B-MgAr z;s7)!@QA5>u^YqiOOu`fg7k;;%*gVgv9@TBHU!wRn%d^R2&;Ge24!5m!T)$+s4MtwEGr@~)`qo@y-dkTh%_1|;uyCjs*T>d7;8kU)b~97HixCTx z$v5J#LJCC787W<@Rp{&}Oktq>ab_A$Q86J|$S(x0#41WI4iOZ9nYnnjPk7#qCeUKd zi5@UfomxgSScb>)O_h81haf>c-Qv!1ME^THvEAyRmVGWm>C);}VF{g-pKy)rsa9{GS>WBb~131kI6IE4l-*=~X ztujLl-)qSmkAPUTw*rYJ4-CKq{;17)+ry<3n5k*BEuzMZ!VY>sVNF&u$r&upT`e`X z_=Ta2DQ62hLOG%DB=2r7mP^~ej!o5i9Sf|U@5+F8E>pxe{8Ev_oH&NKxjpl9cLI+u z+~X&nDtqwI#`r!2QLDDTj6IE!VHSCuu;=5wnIRXQ&NF0OC=jWv(Sle`#uvFnC{WsW z^$Fp-G$WwafL=ruRd2#sGB!?_>&@k5Dl6FBAD&WGZ0;S%j#jt_3n0#HPx(m_~@xhh!wQTbk4u3uQHqXp#4k$Me8tU|9%i*k_p0sLZS z(D@5-Gfycg8+eWm#V}zouji`^EUK{-wkav2nwfVG+(w7WN@Gn)UzNWGXov?Bc7L%< zUD|}c%C6G$;IC+0Ar{4y?C$HdgwltZb^sYEp>;(uwlY`=gyXT5MT|qPW?W z{Uhdhqx|%XpZkTM(Z==0@o+`GvoH7q+f{K$VdX@R>>PU0r;^fr7|1Ghdy?k3cm!pS z4~$+LU`btEvsJ0?ttY(X4#cw z<#q-A>b~IAKvjE3xq;6z?E%^6t+ zfDIwL>dEE03WkOV?2K5T`BI>yD7V-0UF@|qhIl-;F`QGr8$U@J^Gvu@l9+1}kEhXz zF2(v|tXw&1zg+H?2N8C(nm$iP)vSftQGEK>LZ4%zOytF=`PK{e9odb8LN4r71^tx| z;~VWQwc~>K!&M#D{?q9KILx2LPZdUg(wn8&65rbyuW8t{JXEH;ubuFzqy8w%3v>6u zTkqi$S!m}ga7A@&e7r-6E&H;Z z%zXGi$>b-uzV5~&pW5O*QH$=z2$9itWrB#a9F^S&>OYLHAMw0SUD(fF7D3?KWmr0HDdNdRp zC&{)0A|@~T`?f9bISf?v3wWk{Q|rh)mXUZ)e!WiF&Le5V69)o4V#fxjX!y6CCm9x& z4E=bRrJ2>=iWd*8Xzr*uFOH)^XPv1i;_`wCf{EU*E}~>OIq1&o zufVe^lF#fs+oV6Ot*9{CUw9)yPh?F|P5QpnF~B!Ux#n_}R19uq}p`)?K4XX1fJ6}eP%Bejl*Cj99Ia|5}G zAc)r0gOD!{6Fx#6ZRrarp(9--*O(119~BSor29?np$vM3p>37MC3X6#2qnN+9^-Pe z3#R-({jr>I7$3jA^Ozg=NMEP>IrGGcK)f{5!R)yARGr5pbSCwnZiLS4Raf%e=Et$e zwFOFmWBF`rPe^Q_PI>MaUcsM^EBEUW#yHFQf_10}3BT@4>G>0;C9hKo@@@TwYd*9t zz3(ApavH}%8eHc-7w;?Pu+*kwvuVj0eCEfLq_aeNR&C9rNbPP%2nfZHso&U8qS{A) zSbgbuB_ojXhHzD;v3#?o^#2^I&?o8GD38UKJ(;$6SsA6qy^GlGFV?iy)19W1l<_={ z)$NJ3tbX1Pw;#p(lS`-5KXKF{m8Rg??gHF%v{Ve0$R_%d(L9U*f*_{+;NCQHT1a9Ze$X>;1^mjBJpQrLK$}DxM2`@_q@wJvnD?1Yx;Dh~Iegc5`(NSqc}e(7_Sm}nxp*qJ_p)YKAi z7cW;4uKb;GAu`IzQij#$rF$k4ucs&)&1nniN-h(|Cqef!CK-PkBWA`KeV4v4mpn(2 zf}?aXQa&$t2~T>>P(FTe(y|JRMXB|BQxEi3u9%|(@g&uV5Hmq+Quu*FyngngjhJ}K zZ_A4-{e<6>UV>-T&Bx*tNO>xKY+S3?Bh~PcU*b(L(G~Rah~SB6Lxmt?3_aKU)*3fjUs5Coq0?LWTWF!tI-t3w4$vX-09Nsg3wd%`yA z-r+tzdo3?4FzjE6%JVs~oEl4#dp>g1(6J}g%A5#`*m5XjTO6ym-43BT&|n^)T7&h> zVIZFAeC@9KU@jzVW4m%8g7kZ(pni>l%T;}Hjr1zl)0-T5LiocIGUGL+6dPZU?)ce& zi7zrIR`UTIU7X?_kGh7R&D#TANAfeR)kA))sp?pMn+0sZE3;pEB8%4ZZN#Oc+7rio zYG2?Uch8Sjn{K{qsd!Yba$OqyjoU7<%C=Fr6dEjGYX z#;NgD0lRpPasaWoQ!o^#AOx9HBVtI{thkGZquwnk0YP-gSqJpfzSHYGy8&i&UA(x) zFfomBY$D^soAR&_GI4KU)04|PMK#C0@!k3b9U{y%O^Y`bj4s?}vP*W0< zmJ3oT_+zfL&8#t5ifoipx32^vbC59vYx^}#M%X+UAwxnSQhe;dc&Qii*QFH^X#`?D zI#Ie@EZT43G(|yEwd}#tyv0JI4MMoLR_;a`PL@o$^lBb#FZrd^OA$89IkoE=wHyd2 zjrwDZ6X-sSWJB@WXyX9uS~4lNqF*&uOjJj!%ud0IBDP^V5o1lYjkc4eJLdI5s^9Tf zm|F^Mlq8_m(n(W!^q0NlsXdlH#+kBJbxcVT)Bc6&oh(f-nj$t25YI*4?#r6t4AXYlXr_Vl&#mi;>Pdvq7+5-ITC*ljwV|MQ7-kQSJlhETBu>8G(bC!@5WTGKn2Qp~%jb#5|6XaJE%~D% zn|r$W@blGZ?#VkVt1t3b;oCLDsR_5+JG}F+`*lAcQ4urg2PYHWpgr@a#}>$r%b~31qe1y8{g&ysyej)}1QW zq94^P<|IZvzP%PdZ`Nbfo6SjJo6w-Puj*mcm&WIdMOirxU#%d8GL;e|wTlGx{3e;O z0b83O!&h+E{W^}?qPMKZVkvpXG2sYq5v~*|Q&Cvj8nBtI>R{mqpq1SfqLuKT-vu>& zS(HB+D9EO{7Gjr^8YR73hOK8yRK*OAD0puBuc~zT94{sYZIIBr)YY>iJ3&o zVbbIS*pTD+x`&)*C4$-rhmZ<6DB$wshu>g8C=EiJT+eCKezXK-U<@H~3HTVM5<%Ye zBmdAkf-6wHEZ4OVHce-2fh9wn&QX2NT~YL1t9%cv>gQUS4%Qy>6*eo6Z|Wi}X`KFn zTM)Y(sLTHH*dI|i>;1uo>oh@6&pa_ae~Zt>&1H>VVVBMjboqizvIJWs{#eTVNEE+# zgU&)Zd_`?UteTu)0+33MSl==Ty2U3Zk)>uHf>Ho_~mkP_|CwpoVqSk6j(cMvBMRJjZR zOPc=F9S6d*f`Prm50Kl022q={D6;s-Xp@QY)qhtt&6?gSn}l%u%To9aKcUc#_jH=! ziU?zO<_pIl|2*A!_l`zITu4Akc~pB~RdBwZNe*3V)o$4^#N!KP94)Wy4#VZYe>ydLHSw+7FLPCWG+5Tlj&BpbCSb4L<%-Dp?(0?C9tgy6AtaSR3 znG)*aczsgAk$;1Z!CYv=842pwH!RjFr-SKPchAy`NOT5>&JEd#Odj7?(QABm0}=c$ z7OU7?GDBfyY46aJE%m*_vXDHDtF3w=X+PWK4=sfEDTWCns$)(F*moNpZan|Uc|e89 z${bc-;2AO1Ts}BqzQ}4rNUzeT_@L9~w}lbKK+J~LS`=KqY-45i4FSn$j4Il5h~|~t z-Q|O6qVbi!G8SsPmt|{iJ&tm^fNfFA_LE61$r938f_b;a?VKSFkTE-D&XwZ0a@XkmZ--+LkU zuuDg!BU6VaX_y5UxKV|WAXc;lf_GQcGQx42Y0Mq%Ner$BnYY4-liPiY&L#>1uI`&k zBd{VV|Lx961C-f38IiC>Jd7X%K|+6xhwS0ftS8Z|XHP57PwgzncDC@fV(|=JqnT}( zJ`k@##(jaUJN0m=Y)!Fd(;h7xJ8{!-%Ze!XdF&H*63vj=`@*o=7t5gqOOIzQdZQhQwMYbtA86AB1nLrU%a*&=gv>>3E9g1{K>7(E8kW3u zCclP}+m!b0W>(bQ!;|55t&bHRp>ur~jnZ21eD z)}X-JPdQD0Ze98Q<@z;?A$obd27n-ey`4I(T397gqdkP5THs<$b}GMW3KuaMZ_q$H zOgB?hdi?FqDH%P{*3hd_>MJHnmd(i@n@M{5h1kn@-ip~@(na0b@(!WZzb@7ohZ+w# zG-iJAaS$#kujke9L_e1!}qtZ?vkq%lW<5?wQ0Ks}KoLF7(~=rtdw zbkIa>Zgc-MWp$Eg^xh`LDR3*f&O%CqrRaLd#Jbk2bK)#Z5}aO@8h;JK_M_YK@e}|JPi#o>X*LurytJVoJ9jnjr5-X5+fVLs1sf(xjkz zV#<3yuPC!BdxIR*F(g~X$3Z4DpL=9o;;v;Ayc5$kSOfo(7do=@KUj-zXW0IVS-Gv9 zp;GOkNTsU!$xgVSKK4papY+i;{S7j7v8p*vxlmwd)e9lT110Voja!5}ad6gqetspx z)s|>mL0DE94!=>Il(y!4zY^&q`aUmg&ptaL@vSE=lbOp=Ai=j_RAS%`#zvJNX7uKW zW9=M^P=zbvK`){!-`RMBAT#>7f7Okpw&3+yQKtK5fXTg4hA=_SPG1bzkoX_zD}o&^G;0qwWV&#(Z<=4&M4f47Id+Otuo6J%+bC6f}oyf|=4N zInh)qei{VO=2uBkMm;JZ{7+J0L6pRdc|Skfa-eILm)j-zCWr-B4lBPhC1l=^!d-^z zOWoDN3HRba4FYi)+UXji+|3P3%w}^fwpUJ9q=F@rYYURQr+-yd6D+?iE)AwiOKybR zMl!}8`$?4~Nb+DL>{Fekj!(0fZS)Mo_>2wDMG?6jJun;_oi2qFj?1~JH-be9h-n91 z0<5ilo?{?oX-j(W#3zrV7)xAHW%3du<-$>%al4?-*@ zv`bRBN|gi@%+Z3~0>o>|(!7x;X=4j%%jgR;zxd3r>BgBS$IQ3l*JTnHoG#acQ6c5G zs%=Z(laDf!s(ufm7-g<0oR`jA<9~y|k}T6!B955skz}M8W3DP88U$rJ6;=;LcPH;> zHs*(W&L_g+1Y@Hb8cx=_2T%(dU778dUt?0_#k-=^Aa_n)oZcGvt>~x(_vu^5=Lx!{ zu4?bvA*L_I$}k|^t3Rb@&4|6ULOK=u2=!XN>L5kOiM4RC({15oL9(R6Lv_fb#nlpm zsvAEu>@Zo3j6N#?V%J2K*2%k}t!XRLo2T=Ky{{B{ayc2|JdA2{JEle*D0w1Xh-sN-f~;Rn(`Hv$ zZJEHusXk!&I$S@^Hz25qKTLt4Qy4z6b4Z5;E~Ua*L^hTuH;g!OM|-Tscin$CTt6LM ze-qda2x{q$iF%LqTo*rK3cIicUjB-&`xQ=2kL2zAvEJ5e@aj~9lL-2;?SeUXvX@W- zgNDQoMrZzI*Tc!Rqx+}wC1n90?6|Pz)i+98mW*0H5$)Qq6g#Hs^q*>Fq&3H>q>(3RBsFTiNi5&eruJ{D=`#7^&a&R9Nc?3hgT`NA{WO0^>T* z-APovMPKR=?aJo!V2r0VS8{f( z2V9t2)kdO{(@qoZb)QQeUiR|%mb}V>mCJ=d11Mm zu+*=kV`=P`^nKy%J~Q8GHpH5;?fVaS;?qh8-PQziztmtP z57Lgl9XW-EA3!zc> z&$*9!ARZS|fr|8uyJFLM!}hNz-XsN8CYoHKNXA|~iFn8j-^3Z07{>Seh9T$50a>lM z$^Lm*y`4Uhau-26*q+Qp$Ee4dmGT|eY&KZl%mWS9dPB}pj$O10q-Dr3sXA`yZ8H;p zlW8g9^r$dohZRV@={lf=#{~0ZyLLJ+J_p1pkf!Fx4`pPaC~-fdk+**QF~D1OaziVV zj{kO7I*MH!1t-30$ITW{^Zua>+bu z?_Hy=ezs(Lp06FiXg*ik4l!+{c~eIc(Wz}VM+qrUj4qvDcmiI&xVj}$k$WUtx#+ObQvH_t%5Sp%tDEk@SL!N+2-iMmG4Dlm<#!S{t&)^*tdprZ>VfgS*7(4)4-$%bTgcaT(Z>#~>0Y+OFb3*--)`~fdr|A$f2lMAHRsqhp-^rXs2#FwL-voKJo<(LH!5{zGe+up|L z&&pY`P40(fD~}NVINfQ@S0Yy!DQIl{zCTwZO%WTt-QY=R54O;?t8gpW$H_*OSaPhW z(j5POQQJO`Cu+<}&ZJXQDZPWWGyCh#vn~~lAF1%er3H0kXW04Ya1<5eLY16CE7ApGZvw*cJC!bc^j~a(sZ7X z2WslseH9n53~tt}GuUTER_Q68N|O z+WN|%IHG3l1PBQd2rfZ`TX0>R#R(D?2p-(sJrG=j6M`-Tmjn;)zPNjEcUTs^lX~Cp z`*G`TZPlKdnmu!-Pxn;!)6aCL-(Rpp=`B!Rcu-10rno>oH}LBjRM zh?VdA4O5N$+i(D&sVMBNsqdJ+*q_tAyi|^gKrt4s`%n4`M?Z-+i1Rrh+7HJ2*(GDp zo+8aL5}ZvcRc`{#%tOde4W_gu5Qg+QJnK8EP$ydByz6E<@8-VgsAH)G2(M|;3XOKG zBne1hQIPcJtAv%YCp#pHREOa&oQAINCJecR;m`Hg?W-~4134bCg!Z?Q^0t!?I^C_ zKJ+@Z%w7fpOd6EbNtfI}Mx!_mQDNrt!dPCmGjr=S*tD;&;-c|D%j0eKQRvf^KQfWGwMKcF772l`L)9SZyE^08XShAVK{wXoTHfpguWd)JY`& zd3@kRqgqTMRN||4Dg7uE0D}L<(E-A@geZ{S}{7V3JLNz7?86>IWuu|KBO8W zN-ke4LxQ2^d_3?9mob<)P@+&=ir^ZpZ+y|{U7elC!iv;4Z%l@Gwc+4d;03_FynfOZ z%7=C*6wLWct|&`%ALqd^_#11d+<}8w{J31qKWd6-3ec-?gjrEF`sr+Kg4dL1V>Tbw z!%yxaM4yyg;cBpUq0C1c@p5Ua(O#!cD=-S$QLqciUAq~hrf>AOdsge@Z@Ypi$7l4$ z(BS-fsJa59%J)#VF}X5p0s{*Y30Kd%4+V22)D{~7iCkysNspS4cw`JCRy3z>uxmVJ<=nM!l0epytBFbaMVgeOoh6a^zX+@GHgwjpswO3uJoe5Tk(9>k5x$KmTH@ud z=i*re0MtG|U>Wcei>9R-25_o*ZlPtimmkxNaspak1MyZvma;g7Nb;5r)6jAormhxH zdNkh8p`1BPZKzR+rVltiuKp40-XZ%4z3iuhG;xR-gKvx5W=2GcaD7 z-2H+n+5O@DDs7@>2n)W2Y8LMm#mTLxhe#@nTmphFz3&$th3nO1nRukL<0O&^+$|*3 zwfUF@NfxK!3(=|xs6F3gE2=`k-9my7LG59oWIy!fd9&yqs*Ii9$Z*i_f8bem_9fM1 z`e@5VWky&N@qj$tubej z=U`iGeMxi1WY1fl5Bj7Fad$Qo2G!_!Gsx|N1lV}q4 zaHkO|;Vna102+W#Ia=&-E=^vNM=rSO!7HzXFDyYr)qNM63;}>|qk1r1#OK#H{Gs~n z{Y~(0-YoPZqW~{Ze%G2Vz4K%6sJZj;(o3r}gn$>nXjp*-s>d=1j%-DH=Lk@#^z|Qe zw$Y;-;y49~@9J?@)40uquT}i6J%sbv*XK=@F1Jkz$5oK-+_0-FxJSNxVzc+b9v-1s zx;%gV^3py^63M6%b+cY9#I2i$iiOm#flFV^`~J!rVRGfawX$x$I^Fo37-DK+_3`&C zrDDE!1l5o}|4S$j1F{?w{y9P)jQh^a3nYtqou6(^OAS@JzUPWwSgv^9U$x5!OUNsG zo`7LMU;Z*DVi+LLax9sxRAT~i{0(K0 zF3D6fYFw&g9yR0HaTawI=^qIuCIsEq%$EJF8HwJ4PNY+x>U_J9?i}wqUZqJG!!YMhEazb%z%vv*z^u04N488yK0N(I zQW9QWFfZHNcX5O1DCUP&jv6C;I|!1Z__>*Z{zJ%;g&YPgwa?4p?PoB{Bd+tMUNrpC zb29A5|}faMW(PX1P~xmM5G=w|R27y!Q{DW)OKL-0!E7 zF#u3W&Z0ZN=8yjk*i&OZ`O$|_kxA1YW$?pYys|be{GP4xKKJhJQLX@LOoCn_ss}g+$|kNzrBEj?8!FMY9s6vf$!u)7U~AIOzTk3r?V$wg zR_H_Y`)B2E$u zY@*$P&V6r0;FZfQ8F!5gcTko`Iih(&ayxmWjtBi!x8v4FLNJrr*S*0)mXR0Y<$qpA z)p;a{1c(AZRpG*_I}%+Ze{%B_xZco3m^4|OpVWT%EF3|7B|5`LpqhkZ?wXrv_L?Ge zXYt0-1Osq~6u3bBgc`;9WsgsSd-X&biJ3K4Lp zP9v*+W-oLK`yrPr2jMt>3^iBY&Flg2&c=LOh3f;#sy{O@5CyU{i9Pe8RI}lHiXL@A z{t9_7RKsMvM7ydBmOySF%RG=X^46$l6Vy*YYhW-<)khFdyWfmsaNv$JfAqW;e;zS< zYwld`4N+j^XV`u0PsWG&rkV^>)G;RmTQIqp^o&#DL7)G8u6n59s@Dns{y{B?nqSvq zNoGJhv6!R7YE>lgkFKbRVPm+bA%n-lAW7AsuKCPxF3seZ56IFftQ3YH`=b4kf<&?f z6kHiH0Z4|LJuXm0hXPnYmK+W!f5v@iw z+}c^AX2%^(_y{~jD~=OZ7R-4SoOPONLvcy23nOIqVVFo_v?!pUYC(m$s}~*GzT-UuVG?v$hmz>gkE(lJ72xi z?C+m?A{I36d7+apH*tvQWVU}4!+YlU(G%=ueiq0+(>!rhH?XAX%cq%znmiWpg%U3O z+M8ybn+c?ipV(@2{fwo%%$b2o@D@<6`nvc)X?N=d3VBf*kk|U6%;I)#0cY4 zUnX4JeEh?DZQWlhsXyU!KYXqS#C0Aci{MH<3|-xOVP|U+Y`>P6;tiiMK%NVI8p5v5 zl*d)68)0`U1u)#_sfCG<(ojrY&%_ibJA!|Ay^A=VM6RzCiN(UQqCpcTK_~U zR8#y4Qu?6##a@}_t8?a@0+;sede;_D0U=?=f>`Rt6v&f0t83TZ{ML-*&-Uwhkgdk6 z3PK(_KW(=M5zL2CkS~PY(OmOQ+s|+8CChqh_#5-EBc=U4Q@Pmq_KitFuX}-g#SyA^ zav3vy10aHOd;>>R2F=ROGuTA~gFdWXg+iS4ipq{5OHSeeTd(#I4SzC=in`P8cjakp zTZYp3XiY9dmGcHy1-U*Tn*N1*I?B!H{M0Tv?5G4AO|hteeyNP9A^DZ{R5kL8mz^$I z9j+5}o`aE}#Eyw?Q+Lk7o>#RDHe9m3CwHch+%6cw_qT;{a3g!E}k2)YiNvd)8(=IuLM zrx$*;tC;GKfZK%v;^ri`#t}f5xinD*EmTqLshMDkvAy)+>${ z;|CjvZg7NP0UAErV~V(vse4|st`Of^1p1}n`RgaR6*+sBaJbJ~^zW7vxZx4Dp2mEQ zNGyG=#TJRmu=NdxzX|CefVKzhz_5+#`q(Rc@DHZT*^8d;x8J8=3X(yg9Xo2D$8#cI zneJ-5!h7E_jhk;ephyd}gHX>3@?$|G!zMG4F zZ#V^Oz&+?SC!53LSoim>s;!x~5$e3QWXq`mvTdw1nMQ#tgz*ZMNDe$5YUY$K>2A?a zHx!~`2(0%^;lJ7)Ho98#(*9WSQT8^fr&FuUvo4GeM9xxLEM&remyLAI*^|_~FtJ_i zvR6&9>n+f#E*p``X605{^#FN@-Mn&JdG%pM&U#h0a%3}VU)hzJPQ&i`t?BpqC5q3V z0%N9Q3xKdy43vr298oT(24-wG$wn{k}Wg>-zKH-w%L zVISSv+#%Ee%IcDM9e=JTn1$N^NUTo-&eVBai^RQa=10CL1CS>tJrxE+yMwj54pdR= zk80C|a^%R@-kKKhQ8VS9xYfH-S;F)CNL18q?Ffu_8g`(#mvyQPI)ZZiMj~k2I2}RN!@_wd6_AsFLK(XJ9`)ezxJ_HsgtPIlv?egWO#RBgh zh_%RalzWkz$3z|`JDg3;KdogPmC7%-;}s@IE&h*WO#_k#?fRvsPo7O|a)XZo87MAi_nlX#O_zSD|7*m0W(@c})|E zOIf#jDzmY9zlVBj_8l={(N(6wZHtghvAQ#!Dq^g$W~|w3aQ1wz*&-}W6^{D@6y!;R zCP)mKAOgRkW7aW^Zw&O(OuxsWMN2t!R6oHQ;#wf3OF#kKmg-s&xgG$V4q_aS$Bc!H zuUpvVdDPc5U1LDIx9i#s`mUD_WnyfV;^^nbNhA~G$bAB&F6?(S>u%vc479S29euO= zwD?alWi%&po^0$(e>`|fo0L+1+W^9({OF7Hu4P*|u?38d2d@Z*3?rDx; z>r)Mq16H7T_Ih{KEE^-K(tBUP!m5UadQ(l0ui;^Db^B`vM@?*ezHN95G5y+CAFbn= ze8tTBd&wV?;fF3#@PT16r-ScEO)}0P%%m&e%>SM}srqE=8Wd+DuNbuO3;p7Yk?oT$ z-3okVr$z9`8*{zbjSejm4Fr}I9jVzF986@YlRz_Pe^z{9arv6JZr+gzt{o$6T{`$haTy)*HU;KHbXLAahm z5RCUu+RoXwo_@s!H|8k??H9rs-%e$uJA;DH>b|2q zT^Q=PggNWjBf$kHv3Kw2=pMw~m-CTNkx4i{KckV2-8}1~eDb0k`??uU9Ow)5W)nUe zz^zj7rmPay^1nx=Q5NH1$fayDw+u|4uWS~dk03gBH#o7y(_IKk5>kRR1Lm1#dc)iwui^`lZWP$&fp=H_Uor&>& zU5{cno%xA%cU*0*Mf>ML6TR4^I7N4&+P2rkL>WDj+w)72>YW`91QL!R0Gl zQ^+ubz%FeP`lo=E>|Mb-Ixw1Z5;ZvHxtVbEyY)heOsQ7BP^}3gRTeAE?g7r?aaP|% ztJT`W3($IkM9~8!<7KqcWddD(ReX&f(?{ALA(@#O91Y4V%k$; zQay=DNKCp9xC+r$%62CQ5_zj-xpc&ZD{phT$#&9kL5o5(o9VRx5io%^SCjc4HF)`+ zZSdS_if)((;$VF9g)0_x+>lKc7~Qr;h<_Ws94n8*Uv1xJ$}b_^wi-87>tf3E3W?i% z`1Af$ahUY^X1JwzdU|?MSy{K~trOb0VY=B+3a9tw?n8D%whb12reRQ5@=5OJ05?#= z{-e#z)=s`3?b3!J6szO&H~)v*`0VVbyd7|!+Dw^t^{C13PnhMJKdj04>=tT%WZcoE z>Pa%CT02FY3BgRh-4I)G0I&Oq(DT3}d zbzk}&r9Jwvm)$(Y_(f1sU}lSI+(hM911%&4pp4MT2c@SHz~r=hf6xP0Z{z1l&yF2K z*tm3;bt)wOI~11Uik$FoV1*S)(~J7wA=@5~9B^nNUyyMEs2cabNB;2F{fiE5uL2dPM9LmzLhmTlrnU_=nSZ zqkxlkcZd$DZZ0QPymoWLuoit@jLpsUfVZs-geD z<=!OXW+M(;@ezaQ{bui5hs~{2L(%`PO}Ear!@4#h@nZ6CBzABF9>YYTN_KYA?hWsE zE;DrC?}CQ@h%KW5%Fow_^P5{+Y7mI)|65sQ>n$@@*ap!-rcSvg&{!1CQpzfitMC(7 z;tiG%yBfZgg9Fg(%I+@6o$VZ?7xf?$P22$#q5y>SSnf2fp~dOEDQ6B#zlzGQ_eD6% z#F8PR@~_(7MZWttF_EF~8F1O0y1EoJJo?immr?RME;4)n+1LF9T6z-p_V4)?*=e-b zod2BOJv1yI`%$d^GD22NVeL49UjS!wdY-NUTh$Tt0$}Rl#y!>Lr4SGja@owVdSC6k zeLD9LAS;3}D+}%AOQk!^RvWbXBB){T!cWbfoG#nLx%v5<7KH>6;IV_NJFzpZ_13#% z8hms$4Gp)sK@Ou%6o3>iWni#k#M1Sb{^;d71|E6{2PqscJ0p|%$IB?jNbtLO$s;;iywUZry)m~?}_^hb7IJdPm%aTWd z7B{z|B37eRlha{Em{z{?uVaoXV^LAj=i{Y6;CEsCH4cEuy!dATs{}z*r<;i?P*q0H zvmPQZKqH@f9+cNR{?QxG1f~9Q*_l{biIvIg*ts}633&S9b+JwT3aNg{8H0p`hb{Pa9~a(r|RyV-@DqM=qnyvg(4;%fT`ex4iHk=^L1PW%2*~#<(R>n^SSh#%Rc8;7!`F|xE h|NmwCe~r$bo<)tI^F@J_+MWR)d1+;Eh2-a;{{bNJ7}x*+ literal 0 HcmV?d00001 diff --git a/scanpy/tests/_images/master_ranked_genes_matrixplot_swap_axes_vcenter.png b/scanpy/tests/_images/master_ranked_genes_matrixplot_swap_axes_vcenter.png new file mode 100644 index 0000000000000000000000000000000000000000..2e81f228fd4a9407cdbb30794c0391120f807578 GIT binary patch literal 29380 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 literal 0 HcmV?d00001 diff --git a/scanpy/tests/_images/master_umap_layer.png b/scanpy/tests/_images/master_umap_layer.png index ddd8576ed8f79583fdf7275a0b17d88f3ea6eecf..897389de8bc5bd020a89f297fb5c07a43544c96a 100644 GIT binary patch literal 26768 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 diff --git a/scanpy/tests/test_embedding_plots.py b/scanpy/tests/test_embedding_plots.py index 49746e45c3..58db03d965 100644 --- a/scanpy/tests/test_embedding_plots.py +++ b/scanpy/tests/test_embedding_plots.py @@ -2,6 +2,7 @@ from pathlib import Path import matplotlib.pyplot as plt +from matplotlib.colors import Normalize from matplotlib.testing.compare import compare_images import numpy as np import pandas as pd @@ -124,8 +125,27 @@ def groupsfunc(request): @pytest.fixture( - params=[(None, None), (0, 5), ("p15", "p90")], - ids=["vbounds.default", "vbound.numbers", "vbound.percentile"], + params=[ + pytest.param( + {"vmin": None, "vmax": None, "vcenter": None, "norm": None}, + id="vbounds.default", + ), + pytest.param( + {"vmin": 0, "vmax": 5, "vcenter": None, "norm": None}, id="vbounds.numbers" + ), + pytest.param( + {"vmin": "p15", "vmax": "p90", "vcenter": None, "norm": None}, + id="vbounds.percentile", + ), + pytest.param( + {"vmin": 0, "vmax": "p99", "vcenter": 0.1, "norm": None}, + id="vbounds.vcenter", + ), + pytest.param( + {"vmin": None, "vmax": None, "vcenter": None, "norm": Normalize(0, 5)}, + id="vbounds.norm", + ), + ] ) def vbounds(request): return request.param @@ -169,7 +189,7 @@ def test_missing_values_continuous( # Passing through a dict so it's easier to use default values kwargs = {} - kwargs["vmin"], kwargs["vmax"] = vbounds + kwargs.update(vbounds) kwargs["legend_loc"] = legend_loc if na_color is not None: kwargs["na_color"] = na_color diff --git a/scanpy/tests/test_plotting.py b/scanpy/tests/test_plotting.py index c38e3eea0b..b83d40caa3 100644 --- a/scanpy/tests/test_plotting.py +++ b/scanpy/tests/test_plotting.py @@ -594,6 +594,21 @@ def test_correlation(image_comparer): cmap='bwr', ), ), + ( + "ranked_genes_heatmap_swap_axes_vcenter", + partial( + sc.pl.rank_genes_groups_heatmap, + n_genes=20, + swap_axes=True, + use_raw=False, + show_gene_labels=False, + show=False, + vmin=-3, + vcenter=1, + vmax=3, + cmap='RdBu_r', + ), + ), ( "ranked_genes_stacked_violin", partial( @@ -622,6 +637,22 @@ def test_correlation(image_comparer): show=False, ), ), + ( + "ranked_genes_dotplot_logfoldchange_vcenter", + partial( + sc.pl.rank_genes_groups_dotplot, + n_genes=4, + values_to_plot="logfoldchanges", + vmin=-5, + vcenter=1, + vmax=5, + min_logfoldchange=3, + cmap='RdBu_r', + swap_axes=True, + title='log fold changes swap_axes', + show=False, + ), + ), ( "ranked_genes_matrixplot", partial( @@ -645,6 +676,21 @@ def test_correlation(image_comparer): title='log fold changes swap_axes', ), ), + ( + "ranked_genes_matrixplot_swap_axes_vcenter", + partial( + sc.pl.rank_genes_groups_matrixplot, + n_genes=5, + show=False, + swap_axes=True, + values_to_plot='logfoldchanges', + vmin=-6, + vcenter=1, + vmax=6, + cmap='bwr', + title='log fold changes swap_axes', + ), + ), ( "ranked_genes_tracksplot", partial( @@ -748,6 +794,19 @@ def pbmc_scatterplots(): cmap='seismic', ), ), + ( + 'multipanel_vcenter', + partial( + sc.pl.pca, + color=['CD3D', 'CD79A'], + components=['1,2', '1,3'], + vmax=5, + use_raw=False, + vmin=-5, + vcenter=1, + cmap='seismic', + ), + ), ( 'pca_sparse_layer', partial(sc.pl.pca, color=['CD3D', 'CD79A'], layer='sparse', cmap='viridis'), @@ -791,6 +850,7 @@ def pbmc_scatterplots(): title=['gene1', 'gene2'], layer='test', vmin=100, + vcenter=101, ), ), ( @@ -833,26 +893,106 @@ def test_scatter_embedding_groups_and_size(image_comparer): save_and_compare_images('master_embedding_groups_size') -def test_scatter_embedding_add_outline_vmin_vmax(image_comparer): +def test_scatter_embedding_add_outline_vmin_vmax_norm(image_comparer, check_same_image): save_and_compare_images = image_comparer(ROOT, FIGS, tol=15) pbmc = sc.datasets.pbmc68k_reduced() sc.pl.embedding( pbmc, 'X_umap', - color=['percent_mito', 'n_counts', 'bulk_labels'], + color=['percent_mito', 'n_counts', 'bulk_labels', 'percent_mito'], s=200, frameon=False, add_outline=True, - vmax=['p99.0', partial(np.percentile, q=90)], + vmax=['p99.0', partial(np.percentile, q=90), None, 0.03], vmin=0.01, + vcenter=[0.015, None, None, 0.025], outline_color=('#555555', '0.9'), outline_width=(0.5, 0.5), cmap='viridis_r', alpha=0.9, + wspace=0.5, ) save_and_compare_images('master_embedding_outline_vmin_vmax') + import matplotlib as mpl, matplotlib.pyplot as plt + + norm = mpl.colors.LogNorm() + with pytest.raises( + ValueError, match="Passing both norm and vmin/vmax/vcenter is not allowed." + ): + sc.pl.embedding( + pbmc, + 'X_umap', + color=['percent_mito', 'n_counts'], + norm=norm, + vmin=0, + vmax=1, + vcenter=0.5, + cmap='RdBu_r', + ) + + try: + from matplotlib.colors import TwoSlopeNorm as DivNorm + except ImportError: + # matplotlib<3.2 + from matplotlib.colors import DivergingNorm as DivNorm + + from matplotlib.colors import Normalize + + norm = Normalize(0, 10000) + divnorm = DivNorm(200, 150, 6000) + + # allowed + sc.pl.umap( + pbmc, + color=['n_counts', 'bulk_labels', 'percent_mito'], + frameon=False, + vmax=['p99.0', None, None], + vcenter=[0.015, None, None], + norm=[None, norm, norm], + wspace=0.5, + ) + + sc.pl.umap( + pbmc, + color=['n_counts', 'bulk_labels'], + frameon=False, + norm=norm, + wspace=0.5, + ) + plt.savefig(FIGS / 'umap_norm_fig0.png') + plt.close() + + sc.pl.umap( + pbmc, + color=['n_counts', 'bulk_labels'], + frameon=False, + norm=divnorm, + wspace=0.5, + ) + plt.savefig(FIGS / 'umap_norm_fig1.png') + plt.close() + + sc.pl.umap( + pbmc, + color=['n_counts', 'bulk_labels'], + frameon=False, + vcenter=200, + vmin=150, + vmax=6000, + wspace=0.5, + ) + plt.savefig(FIGS / 'umap_norm_fig2.png') + plt.close() + + check_same_image(FIGS / 'umap_norm_fig1.png', FIGS / 'umap_norm_fig2.png', tol=1) + + with pytest.raises(AssertionError): + check_same_image( + FIGS / 'umap_norm_fig1.png', FIGS / 'umap_norm_fig0.png', tol=1 + ) + def test_timeseries(): adata = sc.datasets.pbmc68k_reduced() From 8fe28979f7d4fcbd142b45f69de8774fd9e36e3f Mon Sep 17 00:00:00 2001 From: Zethson Date: Wed, 24 Feb 2021 13:23:43 +0100 Subject: [PATCH 38/85] add flake8 pre-commit Signed-off-by: Zethson --- .flake8 | 7 + .pre-commit-config.yaml | 4 + docs/extensions/function_images.py | 4 +- docs/extensions/github_links.py | 4 +- pyproject.toml | 4 +- scanpy/_settings.py | 25 +-- scanpy/_utils.py | 78 ++----- scanpy/cli.py | 24 +-- scanpy/datasets/_datasets.py | 20 +- scanpy/datasets/_ebi_expression_atlas.py | 4 +- scanpy/external/exporting.py | 74 ++----- scanpy/external/pl.py | 8 +- scanpy/external/pp/_hashsolo.py | 134 ++++-------- scanpy/external/pp/_magic.py | 11 +- scanpy/external/pp/_mnn_correct.py | 11 +- scanpy/external/pp/_scanorama_integrate.py | 4 +- scanpy/external/pp/_scrublet.py | 28 +-- scanpy/external/pp/_scvi.py | 12 ++ scanpy/external/tl/_palantir.py | 4 +- scanpy/external/tl/_phate.py | 3 +- scanpy/external/tl/_phenograph.py | 11 +- scanpy/external/tl/_trimap.py | 3 +- scanpy/external/tl/_wishbone.py | 22 +- scanpy/get/get.py | 21 +- scanpy/logging.py | 12 +- scanpy/neighbors/__init__.py | 101 +++------ scanpy/plotting/_anndata.py | 152 ++++---------- scanpy/plotting/_baseplot_class.py | 42 +--- scanpy/plotting/_dotplot.py | 31 +-- scanpy/plotting/_preprocessing.py | 4 +- scanpy/plotting/_qc.py | 10 +- scanpy/plotting/_stacked_violin.py | 34 +-- scanpy/plotting/_tools/__init__.py | 48 ++--- scanpy/plotting/_tools/paga.py | 138 +++--------- scanpy/plotting/_tools/scatterplots.py | 141 ++++--------- scanpy/plotting/_utils.py | 70 ++----- scanpy/plotting/palettes.py | 4 +- scanpy/preprocessing/_combat.py | 33 +-- scanpy/preprocessing/_deprecated/__init__.py | 10 +- .../_deprecated/highly_variable_genes.py | 20 +- .../preprocessing/_highly_variable_genes.py | 65 ++---- scanpy/preprocessing/_normalization.py | 8 +- scanpy/preprocessing/_pca.py | 16 +- scanpy/preprocessing/_qc.py | 51 ++--- scanpy/preprocessing/_recipes.py | 16 +- scanpy/preprocessing/_simple.py | 78 ++----- scanpy/preprocessing/_utils.py | 4 +- scanpy/queries/_queries.py | 15 +- scanpy/readwrite.py | 65 ++---- scanpy/tests/conftest.py | 8 +- scanpy/tests/external/test_hashsolo.py | 4 +- scanpy/tests/external/test_wishbone.py | 4 +- scanpy/tests/helpers.py | 6 + .../notebooks/test_paga_paul15_subsampled.py | 7 +- scanpy/tests/notebooks/test_pbmc3k.py | 15 +- scanpy/tests/test_combat.py | 4 +- scanpy/tests/test_datasets.py | 13 +- scanpy/tests/test_embedding_plots.py | 48 ++--- scanpy/tests/test_filter_rank_genes_groups.py | 4 +- scanpy/tests/test_get.py | 20 +- scanpy/tests/test_highly_variable_genes.py | 40 +--- scanpy/tests/test_ingest.py | 4 +- scanpy/tests/test_neighbors.py | 8 +- scanpy/tests/test_neighbors_key_added.py | 8 +- scanpy/tests/test_package_structure.py | 4 +- scanpy/tests/test_pca.py | 12 +- scanpy/tests/test_plotting.py | 60 ++---- scanpy/tests/test_preprocessing.py | 45 +--- .../tests/test_preprocessing_distributed.py | 4 +- scanpy/tests/test_qc_metrics.py | 37 +--- scanpy/tests/test_queries.py | 8 +- scanpy/tests/test_rank_genes_groups.py | 50 ++--- scanpy/tests/test_rank_genes_groups_logreg.py | 4 +- scanpy/tests/test_read_10x.py | 8 +- scanpy/tests/test_score_genes.py | 23 +- scanpy/tools/_dendrogram.py | 12 +- scanpy/tools/_diffmap.py | 4 +- scanpy/tools/_dpt.py | 198 ++++-------------- scanpy/tools/_draw_graph.py | 4 +- scanpy/tools/_embedding_density.py | 7 +- scanpy/tools/_ingest.py | 48 ++--- scanpy/tools/_louvain.py | 11 +- scanpy/tools/_marker_gene_overlap.py | 52 ++--- scanpy/tools/_paga.py | 69 ++---- scanpy/tools/_rank_genes_groups.py | 68 ++---- scanpy/tools/_score_genes.py | 20 +- scanpy/tools/_sim.py | 107 +++------- scanpy/tools/_top_genes.py | 30 ++- scanpy/tools/_tsne_fix.py | 8 +- scanpy/tools/_umap.py | 16 +- scanpy/tools/_utils.py | 17 +- scanpy/tools/_utils_clustering.py | 12 +- 92 files changed, 751 insertions(+), 2068 deletions(-) diff --git a/.flake8 b/.flake8 index 636b14206b..c049b35a37 100644 --- a/.flake8 +++ b/.flake8 @@ -1,5 +1,12 @@ +<<<<<<< HEAD # Can't yet be moved to the pyproject.toml due to https://gitlab.com/pycqa/flake8/-/issues/428#note_251982786 [flake8] max-line-length = 88 # switched off since they conflict with black's standards ignore = F401, W503, E501, E203, E231, W504, E402, E126, E712, E741, E266, E262 +======= +[flake8] +exclude = docs, scanpy/tests +max-line-length = 120 +ignore = F401, W503, E501, E203, E231, W504 +>>>>>>> 7a096bf9 (add flake8 pre-commit) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 861b71dbfc..1c19e2cfaf 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,3 +7,7 @@ repos: rev: 3.8.4 hooks: - id: flake8 +<<<<<<< HEAD +======= + exclude: scanpy/tests/ +>>>>>>> 7a096bf9 (add flake8 pre-commit) diff --git a/docs/extensions/function_images.py b/docs/extensions/function_images.py index 42aac73c26..f2a72840f1 100644 --- a/docs/extensions/function_images.py +++ b/docs/extensions/function_images.py @@ -6,9 +6,7 @@ from sphinx.ext.autodoc import Options -def insert_function_images( - app: Sphinx, what: str, name: str, obj: Any, options: Options, lines: List[str] -): +def insert_function_images(app: Sphinx, what: str, name: str, obj: Any, options: Options, lines: List[str]): path = app.config.api_dir / f'{name}.png' if what != 'function' or not path.is_file(): return diff --git a/docs/extensions/github_links.py b/docs/extensions/github_links.py index a2863627c0..f01106fc4b 100644 --- a/docs/extensions/github_links.py +++ b/docs/extensions/github_links.py @@ -32,9 +32,7 @@ def __call__( def register_links(app: Sphinx, config: Config): - gh_url = 'https://github.com/{github_user}/{github_repo}'.format_map( - config.html_context - ) + gh_url = 'https://github.com/{github_user}/{github_repo}'.format_map(config.html_context) app.add_role('pr', AutoLink('pr', f'{gh_url}/pull/{{}}', 'PR {}')) app.add_role('issue', AutoLink('issue', f'{gh_url}/issues/{{}}', 'issue {}')) app.add_role('noteversion', AutoLink('noteversion', f'{gh_url}/releases/tag/{{}}')) diff --git a/pyproject.toml b/pyproject.toml index 436e934584..d516c123eb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -128,8 +128,8 @@ source = ['scanpy'] omit = ['*/tests/*'] [tool.black] -line-length = 88 -target-version = ['py36'] +line-length = 120 +target-version = ['py38'] skip-string-normalization = true exclude = ''' /build/.* diff --git a/scanpy/_settings.py b/scanpy/_settings.py index e217b36083..fdb4ba3c10 100644 --- a/scanpy/_settings.py +++ b/scanpy/_settings.py @@ -53,9 +53,7 @@ def _type_check(var: Any, varname: str, types: Union[type, Tuple[type, ...]]): possible_types_str = types.__name__ else: type_names = [t.__name__ for t in types] - possible_types_str = "{} or {}".format( - ", ".join(type_names[:-1]), type_names[-1] - ) + possible_types_str = "{} or {}".format(", ".join(type_names[:-1]), type_names[-1]) raise TypeError(f"{varname} must be of type {possible_types_str}") @@ -141,9 +139,7 @@ def verbosity(self) -> Verbosity: @verbosity.setter def verbosity(self, verbosity: Union[Verbosity, int, str]): - verbosity_str_options = [ - v for v in _VERBOSITY_TO_LOGLEVEL if isinstance(v, str) - ] + verbosity_str_options = [v for v in _VERBOSITY_TO_LOGLEVEL if isinstance(v, str)] if isinstance(verbosity, Verbosity): self._verbosity = verbosity elif isinstance(verbosity, int): @@ -152,8 +148,7 @@ def verbosity(self, verbosity: Union[Verbosity, int, str]): verbosity = verbosity.lower() if verbosity not in verbosity_str_options: raise ValueError( - f"Cannot set verbosity to {verbosity}. " - f"Accepted string values are: {verbosity_str_options}" + f"Cannot set verbosity to {verbosity}. " f"Accepted string values are: {verbosity_str_options}" ) else: self._verbosity = Verbosity(verbosity_str_options.index(verbosity)) @@ -185,10 +180,7 @@ def file_format_data(self, file_format: str): _type_check(file_format, "file_format_data", str) file_format_options = {"txt", "csv", "h5ad"} if file_format not in file_format_options: - raise ValueError( - f"Cannot set file_format_data to {file_format}. " - f"Must be one of {file_format_options}" - ) + raise ValueError(f"Cannot set file_format_data to {file_format}. " f"Must be one of {file_format_options}") self._file_format_data = file_format @property @@ -293,10 +285,7 @@ def cache_compression(self) -> Optional[str]: @cache_compression.setter def cache_compression(self, cache_compression: Optional[str]): if cache_compression not in {'lzf', 'gzip', None}: - raise ValueError( - f"`cache_compression` ({cache_compression}) " - "must be in {'lzf', 'gzip', None}" - ) + raise ValueError(f"`cache_compression` ({cache_compression}) " "must be in {'lzf', 'gzip', None}") self._cache_compression = cache_compression @property @@ -475,9 +464,7 @@ def _is_run_from_ipython(): def __str__(self) -> str: return '\n'.join( - f'{k} = {v!r}' - for k, v in inspect.getmembers(self) - if not k.startswith("_") and not k == 'getdoc' + f'{k} = {v!r}' for k, v in inspect.getmembers(self) if not k.startswith("_") and not k == 'getdoc' ) diff --git a/scanpy/_utils.py b/scanpy/_utils.py index a2749c3e1b..68ab0695c5 100644 --- a/scanpy/_utils.py +++ b/scanpy/_utils.py @@ -54,9 +54,7 @@ def check_versions(): # make this a warning, not an error # it might be useful for people to still be able to run it - logg.warning( - f'Scanpy {__version__} needs umap ' f'version >=0.3.0, not {umap_version}.' - ) + logg.warning(f'Scanpy {__version__} needs umap ' f'version >=0.3.0, not {umap_version}.') def getdoc(c_or_f: Union[Callable, type]) -> Optional[str]: @@ -77,8 +75,7 @@ def type_doc(name: str): return cls return '\n'.join( - f'{line} : {type_doc(line)}' if line.strip() in sig.parameters else line - for line in doc.split('\n') + f'{line} : {type_doc(line)}' if line.strip() in sig.parameters else line for line in doc.split('\n') ) @@ -122,9 +119,7 @@ def _one_of_ours(obj, root: str): return ( hasattr(obj, "__name__") and not obj.__name__.split(".")[-1].startswith("_") - and getattr( - obj, '__module__', getattr(obj, '__qualname__', obj.__name__) - ).startswith(root) + and getattr(obj, '__module__', getattr(obj, '__qualname__', obj.__name__)).startswith(root) ) @@ -174,9 +169,7 @@ def _check_array_function_arguments(**kwargs): # TODO: Figure out a better solution for documenting dispatched functions invalid_args = [k for k, v in kwargs.items() if v is not None] if len(invalid_args) > 0: - raise TypeError( - f"Arguments {invalid_args} are only valid if an AnnData object is passed." - ) + raise TypeError(f"Arguments {invalid_args} are only valid if an AnnData object is passed.") def _check_use_raw(adata: AnnData, use_raw: Union[None, bool]) -> bool: @@ -216,8 +209,7 @@ def get_igraph_from_adjacency(adjacency, directed=None): pass if g.vcount() != adjacency.shape[0]: logg.warning( - f'The constructed graph has only {g.vcount()} nodes. ' - 'Your adjacency matrix contained redundant nodes.' + f'The constructed graph has only {g.vcount()} nodes. ' 'Your adjacency matrix contained redundant nodes.' ) return g @@ -284,17 +276,12 @@ def compute_association_matrix_of_groups( reference labels, entries are proportional to degree of association. """ if normalization not in {'prediction', 'reference'}: - raise ValueError( - '`normalization` needs to be either "prediction" or "reference".' - ) + raise ValueError('`normalization` needs to be either "prediction" or "reference".') sanitize_anndata(adata) cats = adata.obs[reference].cat.categories for cat in cats: if cat in settings.categories_to_ignore: - logg.info( - f'Ignoring category {cat!r} ' - 'as it’s in `settings.categories_to_ignore`.' - ) + logg.info(f'Ignoring category {cat!r} ' 'as it’s in `settings.categories_to_ignore`.') asso_names = [] asso_matrix = [] for ipred_group, pred_group in enumerate(adata.obs[prediction].cat.categories): @@ -313,15 +300,11 @@ def compute_association_matrix_of_groups( if normalization == 'prediction': # compute which fraction of the predicted group is contained in # the ref group - ratio_contained = ( - np.sum(mask_pred_int) - np.sum(mask_ref_or_pred - mask_ref) - ) / np.sum(mask_pred_int) + ratio_contained = (np.sum(mask_pred_int) - np.sum(mask_ref_or_pred - mask_ref)) / np.sum(mask_pred_int) else: # compute which fraction of the reference group is contained in # the predicted group - ratio_contained = ( - np.sum(mask_ref) - np.sum(mask_ref_or_pred - mask_pred_int) - ) / np.sum(mask_ref) + ratio_contained = (np.sum(mask_ref) - np.sum(mask_ref_or_pred - mask_pred_int)) / np.sum(mask_ref) asso_matrix[-1] += [ratio_contained] name_list_pred = [ cats[i] if cats[i] not in settings.categories_to_ignore else '' @@ -329,18 +312,13 @@ def compute_association_matrix_of_groups( if asso_matrix[-1][i] > threshold ] asso_names += ['\n'.join(name_list_pred[:max_n_names])] - Result = namedtuple( - 'compute_association_matrix_of_groups', ['asso_names', 'asso_matrix'] - ) + Result = namedtuple('compute_association_matrix_of_groups', ['asso_names', 'asso_matrix']) return Result(asso_names=asso_names, asso_matrix=np.array(asso_matrix)) def get_associated_colors_of_groups(reference_colors, asso_matrix): return [ - { - reference_colors[i_ref]: asso_matrix[i_pred, i_ref] - for i_ref in range(asso_matrix.shape[1]) - } + {reference_colors[i_ref]: asso_matrix[i_pred, i_ref] for i_ref in range(asso_matrix.shape[1])} for i_pred in range(asso_matrix.shape[0]) ] @@ -369,16 +347,9 @@ def identify_groups(ref_labels, pred_labels, return_overlaps=False): associated_predictions = {} associated_overlaps = {} for ref_label in ref_unique: - sub_pred_unique, sub_pred_counts = np.unique( - pred_labels[ref_label == ref_labels], return_counts=True - ) - relative_overlaps_pred = [ - sub_pred_counts[i] / pred_dict[n] for i, n in enumerate(sub_pred_unique) - ] - relative_overlaps_ref = [ - sub_pred_counts[i] / ref_dict[ref_label] - for i, n in enumerate(sub_pred_unique) - ] + sub_pred_unique, sub_pred_counts = np.unique(pred_labels[ref_label == ref_labels], return_counts=True) + relative_overlaps_pred = [sub_pred_counts[i] / pred_dict[n] for i, n in enumerate(sub_pred_unique)] + relative_overlaps_ref = [sub_pred_counts[i] / ref_dict[ref_label] for i, n in enumerate(sub_pred_unique)] relative_overlaps = np.c_[relative_overlaps_pred, relative_overlaps_ref] relative_overlaps_min = np.min(relative_overlaps, axis=1) pred_best_index = np.argsort(relative_overlaps_min)[::-1] @@ -502,9 +473,7 @@ def select_groups(adata, groups_order_subset='all', key='groups'): if key + '_masks' in adata.uns: groups_masks = adata.uns[key + '_masks'] else: - groups_masks = np.zeros( - (len(adata.obs[key].cat.categories), adata.obs[key].values.size), dtype=bool - ) + groups_masks = np.zeros((len(adata.obs[key].cat.categories), adata.obs[key].values.size), dtype=bool) for iname, name in enumerate(adata.obs[key].cat.categories): # if the name is not found, fallback to index retrieval if adata.obs[key].cat.categories[iname] in adata.obs[key].values: @@ -516,9 +485,7 @@ def select_groups(adata, groups_order_subset='all', key='groups'): if groups_order_subset != 'all': groups_ids = [] for name in groups_order_subset: - groups_ids.append( - np.where(adata.obs[key].cat.categories.values == name)[0][0] - ) + groups_ids.append(np.where(adata.obs[key].cat.categories.values == name)[0][0]) if len(groups_ids) == 0: # fallback to index retrieval groups_ids = np.where( @@ -600,9 +567,7 @@ def subsample( return Xsampled, rows -def subsample_n( - X: np.ndarray, n: int = 0, seed: int = 0 -) -> Tuple[np.ndarray, np.ndarray]: +def subsample_n(X: np.ndarray, n: int = 0, seed: int = 0) -> Tuple[np.ndarray, np.ndarray]: """Subsample n samples from rows of array. Parameters @@ -751,17 +716,12 @@ def __contains__(self, key): def _choose_graph(adata, obsp, neighbors_key): """Choose connectivities from neighbbors or another obsp column""" if obsp is not None and neighbors_key is not None: - raise ValueError( - 'You can\'t specify both obsp, neighbors_key. ' 'Please select only one.' - ) + raise ValueError('You can\'t specify both obsp, neighbors_key. ' 'Please select only one.') if obsp is not None: return adata.obsp[obsp] else: neighbors = NeighborsView(adata, neighbors_key) if 'connectivities' not in neighbors: - raise ValueError( - 'You need to run `pp.neighbors` first ' - 'to compute a neighborhood graph.' - ) + raise ValueError('You need to run `pp.neighbors` first ' 'to compute a neighborhood graph.') return neighbors['connectivities'] diff --git a/scanpy/cli.py b/scanpy/cli.py index 9a90cebd31..b7529d52e6 100644 --- a/scanpy/cli.py +++ b/scanpy/cli.py @@ -25,9 +25,7 @@ class _DelegatingSubparsersAction(_SubParsersAction): def __init__(self, *args, _command: str, _runargs: Dict[str, Any], **kwargs): super().__init__(*args, **kwargs) self.command = _command - self._name_parser_map = self.choices = _CommandDelegator( - _command, self, **_runargs - ) + self._name_parser_map = self.choices = _CommandDelegator(_command, self, **_runargs) class _CommandDelegator(cabc.MutableMapping): @@ -49,9 +47,7 @@ def __getitem__(self, k: str) -> ArgumentParser: if which(f'{self.command}-{k}'): return _DelegatingParser(self, k) # Only here is the command list retrieved - raise ArgumentError( - self.action, f'No command “{k}”. Choose from {set(self)}' - ) + raise ArgumentError(self.action, f'No command “{k}”. Choose from {set(self)}') def __setitem__(self, k: str, v: ArgumentParser) -> None: self.parser_map[k] = v @@ -74,8 +70,7 @@ def __hash__(self) -> int: def __eq__(self, other: Mapping[str, ArgumentParser]): if isinstance(other, _CommandDelegator): return all( - getattr(self, attr) == getattr(other, attr) - for attr in ['command', 'action', 'parser_map', 'runargs'] + getattr(self, attr) == getattr(other, attr) for attr in ['command', 'action', 'parser_map', 'runargs'] ) return self.parser_map == other @@ -103,9 +98,7 @@ def parse_known_args( args: Optional[Sequence[str]] = None, namespace: Optional[Namespace] = None, ) -> Tuple[Namespace, List[str]]: - assert ( - args is not None and namespace is None - ), 'Only use DelegatingParser as subparser' + assert args is not None and namespace is None, 'Only use DelegatingParser as subparser' return Namespace(func=partial(run, [self.prog, *args], **self.cd.runargs)), [] @@ -115,9 +108,7 @@ def _cmd_settings() -> None: print(settings) -def main( - argv: Optional[Sequence[str]] = None, *, check: bool = True, **runargs -) -> Optional[CompletedProcess]: +def main(argv: Optional[Sequence[str]] = None, *, check: bool = True, **runargs) -> Optional[CompletedProcess]: """\ Run a builtin scanpy command or a scanpy-* subcommand. @@ -125,10 +116,7 @@ def main( `~run(['scanpy', *argv], **runargs)` """ parser = ArgumentParser( - description=( - "There are a few packages providing commands. " - "Try e.g. `pip install scanpy-scripts`!" - ) + description=("There are a few packages providing commands. " "Try e.g. `pip install scanpy-scripts`!") ) parser.set_defaults(func=parser.print_help) diff --git a/scanpy/datasets/_datasets.py b/scanpy/datasets/_datasets.py index 060b00ddd3..ec909b41e6 100644 --- a/scanpy/datasets/_datasets.py +++ b/scanpy/datasets/_datasets.py @@ -136,13 +136,10 @@ def moignard15() -> AnnData: } # annotate each observation/cell adata.obs['exp_groups'] = [ - next(gname for gname in groups.keys() if sname.startswith(gname)) - for sname in adata.obs_names + next(gname for gname in groups.keys() if sname.startswith(gname)) for sname in adata.obs_names ] # fix the order and colors of names in "groups" - adata.obs['exp_groups'] = pd.Categorical( - adata.obs['exp_groups'], categories=list(groups.keys()) - ) + adata.obs['exp_groups'] = pd.Categorical(adata.obs['exp_groups'], categories=list(groups.keys())) adata.uns['exp_groups_colors'] = list(groups.values()) return adata @@ -162,10 +159,7 @@ def paul15() -> AnnData: ------- Annotated data matrix. """ - logg.warning( - 'In Scanpy 0.*, this returned logarithmized data. ' - 'Now it returns non-logarithmized data.' - ) + logg.warning('In Scanpy 0.*, this returned logarithmized data. ' 'Now it returns non-logarithmized data.') import h5py filename = settings.datasetdir / 'paul15/paul15.h5' @@ -335,9 +329,7 @@ def _download_visium_dataset( # Download spatial data tar_filename = f"{sample_id}_spatial.tar.gz" tar_pth = sample_dir / tar_filename - _utils.check_presence_download( - filename=tar_pth, backup_url=url_prefix + tar_filename - ) + _utils.check_presence_download(filename=tar_pth, backup_url=url_prefix + tar_filename) with tarfile.open(tar_pth) as f: for el in f: if not (sample_dir / el.name).exists(): @@ -411,9 +403,7 @@ def visium_sge( spaceranger_version = "1.1.0" else: spaceranger_version = "1.2.0" - _download_visium_dataset( - sample_id, spaceranger_version, download_image=include_hires_tiff - ) + _download_visium_dataset(sample_id, spaceranger_version, download_image=include_hires_tiff) if include_hires_tiff: adata = read_visium( settings.datasetdir / sample_id, diff --git a/scanpy/datasets/_ebi_expression_atlas.py b/scanpy/datasets/_ebi_expression_atlas.py index 013f0aa804..e45e73a18d 100644 --- a/scanpy/datasets/_ebi_expression_atlas.py +++ b/scanpy/datasets/_ebi_expression_atlas.py @@ -86,9 +86,7 @@ def read_expression_from_archive(archive: ZipFile) -> anndata.AnnData: return adata -def ebi_expression_atlas( - accession: str, *, filter_boring: bool = False -) -> anndata.AnnData: +def ebi_expression_atlas(accession: str, *, filter_boring: bool = False) -> anndata.AnnData: """\ Load a dataset from the `EBI Single Cell Expression Atlas `__ diff --git a/scanpy/external/exporting.py b/scanpy/external/exporting.py index a405cbb365..9a36dce538 100644 --- a/scanpy/external/exporting.py +++ b/scanpy/external/exporting.py @@ -75,25 +75,16 @@ def spring_project( embedding_method = 'X_' + embedding_method else: if embedding_method in adata.uns: - embedding_method = ( - 'X_' - + embedding_method - + '_' - + adata.uns[embedding_method]['params']['layout'] - ) + embedding_method = 'X_' + embedding_method + '_' + adata.uns[embedding_method]['params']['layout'] else: - raise ValueError( - 'Run the specified embedding method `%s` first.' % embedding_method - ) + raise ValueError('Run the specified embedding method `%s` first.' % embedding_method) coords = adata.obsm[embedding_method] # Make project directory and subplot directory (subplot has same name as project) # For now, the subplot is just all cells in adata project_dir: Path = Path(project_dir) - subplot_dir: Path = ( - project_dir.parent if subplot_name is None else project_dir / subplot_name - ) + subplot_dir: Path = project_dir.parent if subplot_name is None else project_dir / subplot_name subplot_dir.mkdir(parents=True, exist_ok=True) print(f'Writing subplot to {subplot_dir}') @@ -159,9 +150,7 @@ def spring_project( elif is_categorical(adata.obs[obs_name]): categorical_extras[obs_name] = [str(x) for x in adata.obs[obs_name]] else: - logg.warning( - f'Cell grouping {obs_name!r} is not a categorical variable' - ) + logg.warning(f'Cell grouping {obs_name!r} is not a categorical variable') if custom_color_tracks is None: for obs_name in adata.obs: if not is_categorical(adata.obs[obs_name]): @@ -175,9 +164,7 @@ def spring_project( elif not is_categorical(adata.obs[obs_name]): continuous_extras[obs_name] = np.array(adata.obs[obs_name]) else: - logg.warning( - f'Custom color track {obs_name!r} is not a continuous variable' - ) + logg.warning(f'Custom color track {obs_name!r} is not a continuous variable') # Write continuous colors continuous_extras['Uniform'] = np.zeros(E.shape[0]) @@ -191,12 +178,8 @@ def spring_project( # Write categorical data categorical_coloring_data = {} - categorical_coloring_data = _build_categ_colors( - categorical_coloring_data, categorical_extras - ) - _write_cell_groupings( - subplot_dir / 'categorical_coloring_data.json', categorical_coloring_data - ) + categorical_coloring_data = _build_categ_colors(categorical_coloring_data, categorical_extras) + _write_cell_groupings(subplot_dir / 'categorical_coloring_data.json', categorical_coloring_data) # Write graph in two formats for backwards compatibility edges = _get_edges(adata, neighbors_key) @@ -210,10 +193,7 @@ def spring_project( # Write 2-D coordinates, after adjusting to roughly match SPRING's default d3js force layout parameters coords = coords - coords.min(0)[None, :] - coords = ( - coords * (np.array([1000, 1000]) / coords.ptp(0))[None, :] - + np.array([200, -200])[None, :] - ) + coords = coords * (np.array([1000, 1000]) / coords.ptp(0))[None, :] + np.array([200, -200])[None, :] np.savetxt( subplot_dir / 'coordinates.txt', np.hstack((np.arange(E.shape[0])[:, None], coords)), @@ -343,14 +323,10 @@ def _get_color_stats_genes(color_stats, E, gene_list): for iG in range(E.shape[1]): n_nonzero = E.indptr[iG + 1] - E.indptr[iG] if n_nonzero > pctl_n: - pctls[iG] = np.percentile( - E.data[E.indptr[iG] : E.indptr[iG + 1]], 100 - 100 * pctl_n / n_nonzero - ) + pctls[iG] = np.percentile(E.data[E.indptr[iG] : E.indptr[iG + 1]], 100 - 100 * pctl_n / n_nonzero) else: pctls[iG] = 0 - color_stats[gene_list[iG]] = tuple( - map(float, (means[iG], stdevs[iG], mins[iG], maxes[iG], pctls[iG])) - ) + color_stats[gene_list[iG]] = tuple(map(float, (means[iG], stdevs[iG], mins[iG], maxes[iG], pctls[iG]))) return color_stats @@ -372,10 +348,7 @@ def _write_color_stats(filename, color_stats): def _build_categ_colors(categorical_coloring_data, cell_groupings): for k, labels in cell_groupings.items(): - label_colors = { - l: _frac_to_hex(float(i) / len(set(labels))) - for i, l in enumerate(list(set(labels))) - } + label_colors = {l: _frac_to_hex(float(i) / len(set(labels))) for i, l in enumerate(list(set(labels)))} categorical_coloring_data[k] = { 'label_colors': label_colors, 'label_list': labels, @@ -385,9 +358,7 @@ def _build_categ_colors(categorical_coloring_data, cell_groupings): def _write_cell_groupings(filename, categorical_coloring_data): with open(filename, 'w') as f: - f.write( - json.dumps(categorical_coloring_data, indent=4, sort_keys=True) - ) # .decode('utf-8')) + f.write(json.dumps(categorical_coloring_data, indent=4, sort_keys=True)) # .decode('utf-8')) def _export_PAGA_to_SPRING(adata, paga_coords, outpath): @@ -398,18 +369,14 @@ def _export_PAGA_to_SPRING(adata, paga_coords, outpath): sizes = list(adata.uns[group_key + '_sizes']) clus_labels = adata.obs[group_key].cat.codes.values - cell_groups = [ - [int(j) for j in np.nonzero(clus_labels == i)[0]] for i in range(len(names)) - ] + cell_groups = [[int(j) for j in np.nonzero(clus_labels == i)[0]] for i in range(len(names))] if group_key + '_colors' in adata.uns: colors = list(adata.uns[group_key + '_colors']) else: import scanpy.plotting.utils - scanpy.plotting.utils.add_colors_for_categorical_sample_annotation( - adata, group_key - ) + scanpy.plotting.utils.add_colors_for_categorical_sample_annotation(adata, group_key) colors = list(adata.uns[group_key + '_colors']) # retrieve edge level data @@ -431,9 +398,7 @@ def _export_PAGA_to_SPRING(adata, paga_coords, outpath): # make node list nodes = [] - for i, name, xy, color, size, cells in zip( - range(len(names)), names, coords, colors, sizes, cell_groups - ): + for i, name, xy, color, size, cells in zip(range(len(names)), names, coords, colors, sizes, cell_groups): nodes.append( { 'index': i, @@ -449,9 +414,7 @@ def _export_PAGA_to_SPRING(adata, paga_coords, outpath): links = [] for source, target, weight in zip(sources, targets, weights): if source < target and weight > min_edge_weight_save: - links.append( - {'source': int(source), 'target': int(target), 'weight': float(weight)} - ) + links.append({'source': int(source), 'target': int(target), 'weight': float(weight)}) # save data about edge weights edge_weight_meta = { @@ -564,10 +527,7 @@ def cellbrowser( try: import cellbrowser.cellbrowser as cb except ImportError: - logg.error( - "The package cellbrowser is not installed. " - "Install with 'pip install cellbrowser' and retry." - ) + logg.error("The package cellbrowser is not installed. " "Install with 'pip install cellbrowser' and retry.") raise data_dir = str(data_dir) diff --git a/scanpy/external/pl.py b/scanpy/external/pl.py index 2c4167d72f..01fac05ed6 100644 --- a/scanpy/external/pl.py +++ b/scanpy/external/pl.py @@ -176,9 +176,7 @@ def sam( try: dt = adata.obsm[projection] except KeyError: - raise ValueError( - 'Please create a projection first using run_umap or run_tsne' - ) + raise ValueError('Please create a projection first using run_umap or run_tsne') else: dt = projection @@ -187,9 +185,7 @@ def sam( axes = plt.gca() if c is None: - axes.scatter( - dt[:, 0], dt[:, 1], s=s, linewidth=linewidth, edgecolor=edgecolor, **kwargs - ) + axes.scatter(dt[:, 0], dt[:, 1], s=s, linewidth=linewidth, edgecolor=edgecolor, **kwargs) return axes if isinstance(c, str): diff --git a/scanpy/external/pp/_hashsolo.py b/scanpy/external/pp/_hashsolo.py index ecc3e8e9ac..ca2ab5179a 100644 --- a/scanpy/external/pp/_hashsolo.py +++ b/scanpy/external/pp/_hashsolo.py @@ -66,9 +66,7 @@ def gaussian_updates(data, mu_o, std_o): n = len(data) lam = 1 / np.var(data) if len(data) > 1 else lam_o lam_n = lam_o + n * lam - mu_n = ( - (np.mean(data) * n * lam + mu_o * lam_o) / lam_n if len(data) > 0 else mu_o - ) + mu_n = (np.mean(data) * n * lam + mu_o * lam_o) / lam_n if len(data) > 0 else mu_o return mu_n, (1 / (lam_n / (n + 1))) ** (1 / 2) eps = 1e-15 @@ -91,12 +89,8 @@ def gaussian_updates(data, mu_o, std_o): # barcodes with rank < k are considered to be noise global_signal_counts = np.ravel(data_sort[:, -1]) global_noise_counts = np.ravel(data_sort[:, :-number_of_non_noise_barcodes]) - global_mu_signal_o, global_sigma_signal_o = np.mean(global_signal_counts), np.std( - global_signal_counts - ) - global_mu_noise_o, global_sigma_noise_o = np.mean(global_noise_counts), np.std( - global_noise_counts - ) + global_mu_signal_o, global_sigma_signal_o = np.mean(global_signal_counts), np.std(global_signal_counts) + global_mu_noise_o, global_sigma_noise_o = np.mean(global_noise_counts), np.std(global_noise_counts) noise_params_dict = {} signal_params_dict = {} @@ -104,9 +98,7 @@ def gaussian_updates(data, mu_o, std_o): # for each barcode get empirical noise and signal distribution parameterization for x in np.arange(num_of_barcodes): sample_barcodes = data[:, x] - sample_barcodes_noise_idx = np.where(data_arg[:, :num_of_noise_barcodes] == x)[ - 0 - ] + sample_barcodes_noise_idx = np.where(data_arg[:, :num_of_noise_barcodes] == x)[0] sample_barcodes_signal_idx = np.where(data_arg[:, -1] == x) # get noise and signal counts @@ -114,12 +106,8 @@ def gaussian_updates(data, mu_o, std_o): signal_counts = sample_barcodes[sample_barcodes_signal_idx] # get parameters of distribution, assuming lognormal do update from global values - noise_param = gaussian_updates( - noise_counts, global_mu_noise_o, global_sigma_noise_o - ) - signal_param = gaussian_updates( - signal_counts, global_mu_signal_o, global_sigma_signal_o - ) + noise_param = gaussian_updates(noise_counts, global_mu_noise_o, global_sigma_noise_o) + signal_param = gaussian_updates(signal_counts, global_mu_signal_o, global_sigma_signal_o) noise_params_dict[x] = noise_param signal_params_dict[x] = signal_param @@ -127,9 +115,7 @@ def gaussian_updates(data, mu_o, std_o): counter = 0 # for each combination of noise and signal barcode calculate probiltiy of in silico and real cell hypotheses - for noise_sample_idx, signal_sample_idx in product( - np.arange(num_of_barcodes), np.arange(num_of_barcodes) - ): + for noise_sample_idx, signal_sample_idx in product(np.arange(num_of_barcodes), np.arange(num_of_barcodes)): signal_subset = data_arg[:, -1] == signal_sample_idx noise_subset = data_arg[:, -2] == noise_sample_idx subset = signal_subset & noise_subset @@ -182,15 +168,9 @@ def gaussian_updates(data, mu_o, std_o): + eps ) - probs_of_negative = np.sum( - [log_noise_noise_probs, log_signal_noise_probs], axis=0 - ) - probs_of_singlet = np.sum( - [log_noise_noise_probs, log_signal_signal_probs], axis=0 - ) - probs_of_doublet = np.sum( - [log_noise_signal_probs, log_signal_signal_probs], axis=0 - ) + probs_of_negative = np.sum([log_noise_noise_probs, log_signal_noise_probs], axis=0) + probs_of_singlet = np.sum([log_noise_noise_probs, log_signal_signal_probs], axis=0) + probs_of_doublet = np.sum([log_noise_signal_probs, log_signal_signal_probs], axis=0) log_probs_list = [probs_of_negative, probs_of_singlet, probs_of_doublet] # each cell and each hypothesis probability @@ -226,15 +206,11 @@ def _calculate_bayes_rule(data, priors, number_of_noise_barcodes): "log_likelihoods_for_each_hypothesis" key is a 2d np.array log likelihood of each hypothesis """ priors = np.array(priors) - log_likelihoods_for_each_hypothesis, _, _ = _calculate_log_likelihoods( - data, number_of_noise_barcodes - ) + log_likelihoods_for_each_hypothesis, _, _ = _calculate_log_likelihoods(data, number_of_noise_barcodes) probs_hypotheses = ( np.exp(log_likelihoods_for_each_hypothesis) * priors - / np.sum( - np.multiply(np.exp(log_likelihoods_for_each_hypothesis), priors), axis=1 - )[:, None] + / np.sum(np.multiply(np.exp(log_likelihoods_for_each_hypothesis), priors), axis=1)[:, None] ) most_likely_hypothesis = np.argmax(probs_hypotheses, axis=1) return { @@ -295,9 +271,7 @@ def hashsolo( >>> sce.pp.hashsolo(data, ['Hash1', 'Hash2', 'Hash3']) >>> data.obs.head() """ - print( - "Please cite HashSolo paper:\nhttps://www.cell.com/cell-systems/fulltext/S2405-4712(20)30195-2" - ) + print("Please cite HashSolo paper:\nhttps://www.cell.com/cell-systems/fulltext/S2405-4712(20)30195-2") data = adata.obs[cell_hashing_columns].values if not check_nonnegative_integers(data): @@ -326,67 +300,39 @@ def hashsolo( unique_cluster_features = np.unique(adata.obs[cluster_features]) for cluster_feature in unique_cluster_features: cluster_feature_bool_vector = adata.obs[cluster_features] == cluster_feature - posterior_dict = _calculate_bayes_rule( - data[cluster_feature_bool_vector], priors, number_of_noise_barcodes - ) - results.loc[ - cluster_feature_bool_vector, "most_likely_hypothesis" - ] = posterior_dict["most_likely_hypothesis"] - results.loc[ - cluster_feature_bool_vector, "cluster_feature" - ] = cluster_feature - results.loc[ - cluster_feature_bool_vector, "negative_hypothesis_probability" - ] = posterior_dict["probs_hypotheses"][:, 0] - results.loc[ - cluster_feature_bool_vector, "singlet_hypothesis_probability" - ] = posterior_dict["probs_hypotheses"][:, 1] - results.loc[ - cluster_feature_bool_vector, "doublet_hypothesis_probability" - ] = posterior_dict["probs_hypotheses"][:, 2] + posterior_dict = _calculate_bayes_rule(data[cluster_feature_bool_vector], priors, number_of_noise_barcodes) + results.loc[cluster_feature_bool_vector, "most_likely_hypothesis"] = posterior_dict[ + "most_likely_hypothesis" + ] + results.loc[cluster_feature_bool_vector, "cluster_feature"] = cluster_feature + results.loc[cluster_feature_bool_vector, "negative_hypothesis_probability"] = posterior_dict[ + "probs_hypotheses" + ][:, 0] + results.loc[cluster_feature_bool_vector, "singlet_hypothesis_probability"] = posterior_dict[ + "probs_hypotheses" + ][:, 1] + results.loc[cluster_feature_bool_vector, "doublet_hypothesis_probability"] = posterior_dict[ + "probs_hypotheses" + ][:, 2] else: posterior_dict = _calculate_bayes_rule(data, priors, number_of_noise_barcodes) - results.loc[:, "most_likely_hypothesis"] = posterior_dict[ - "most_likely_hypothesis" - ] + results.loc[:, "most_likely_hypothesis"] = posterior_dict["most_likely_hypothesis"] results.loc[:, "cluster_feature"] = 0 - results.loc[:, "negative_hypothesis_probability"] = posterior_dict[ - "probs_hypotheses" - ][:, 0] - results.loc[:, "singlet_hypothesis_probability"] = posterior_dict[ - "probs_hypotheses" - ][:, 1] - results.loc[:, "doublet_hypothesis_probability"] = posterior_dict[ - "probs_hypotheses" - ][:, 2] - - adata.obs["most_likely_hypothesis"] = results.loc[ - adata.obs_names, "most_likely_hypothesis" - ] + results.loc[:, "negative_hypothesis_probability"] = posterior_dict["probs_hypotheses"][:, 0] + results.loc[:, "singlet_hypothesis_probability"] = posterior_dict["probs_hypotheses"][:, 1] + results.loc[:, "doublet_hypothesis_probability"] = posterior_dict["probs_hypotheses"][:, 2] + + adata.obs["most_likely_hypothesis"] = results.loc[adata.obs_names, "most_likely_hypothesis"] adata.obs["cluster_feature"] = results.loc[adata.obs_names, "cluster_feature"] - adata.obs["negative_hypothesis_probability"] = results.loc[ - adata.obs_names, "negative_hypothesis_probability" - ] - adata.obs["singlet_hypothesis_probability"] = results.loc[ - adata.obs_names, "singlet_hypothesis_probability" - ] - adata.obs["doublet_hypothesis_probability"] = results.loc[ - adata.obs_names, "doublet_hypothesis_probability" - ] + adata.obs["negative_hypothesis_probability"] = results.loc[adata.obs_names, "negative_hypothesis_probability"] + adata.obs["singlet_hypothesis_probability"] = results.loc[adata.obs_names, "singlet_hypothesis_probability"] + adata.obs["doublet_hypothesis_probability"] = results.loc[adata.obs_names, "doublet_hypothesis_probability"] adata.obs["Classification"] = None - adata.obs.loc[ - adata.obs["most_likely_hypothesis"] == 2, "Classification" - ] = "Doublet" - adata.obs.loc[ - adata.obs["most_likely_hypothesis"] == 0, "Classification" - ] = "Negative" + adata.obs.loc[adata.obs["most_likely_hypothesis"] == 2, "Classification"] = "Doublet" + adata.obs.loc[adata.obs["most_likely_hypothesis"] == 0, "Classification"] = "Negative" all_sings = adata.obs["most_likely_hypothesis"] == 1 - singlet_sample_index = np.argmax( - adata.obs.loc[all_sings, cell_hashing_columns].values, axis=1 - ) - adata.obs.loc[all_sings, "Classification"] = adata.obs[ - cell_hashing_columns - ].columns[singlet_sample_index] + singlet_sample_index = np.argmax(adata.obs.loc[all_sings, cell_hashing_columns].values, axis=1) + adata.obs.loc[all_sings, "Classification"] = adata.obs[cell_hashing_columns].columns[singlet_sample_index] return adata if not inplace else None diff --git a/scanpy/external/pp/_magic.py b/scanpy/external/pp/_magic.py index 5690923c5b..aeb382733b 100644 --- a/scanpy/external/pp/_magic.py +++ b/scanpy/external/pp/_magic.py @@ -150,10 +150,7 @@ def magic( start = logg.info('computing MAGIC') all_or_pca = isinstance(name_list, (str, type(None))) if all_or_pca and name_list not in {"all_genes", "pca_only", None}: - raise ValueError( - "Invalid string value for `name_list`: " - "Only `'all_genes'` and `'pca_only'` are allowed." - ) + raise ValueError("Invalid string value for `name_list`: " "Only `'all_genes'` and `'pca_only'` are allowed.") if copy is None: copy = not all_or_pca elif not all_or_pca and not copy: @@ -181,11 +178,7 @@ def magic( logg.info( ' finished', time=start, - deep=( - "added\n 'X_magic', PCA on MAGIC coordinates (adata.obsm)" - if name_list == "pca_only" - else '' - ), + deep=("added\n 'X_magic', PCA on MAGIC coordinates (adata.obsm)" if name_list == "pca_only" else ''), ) # update AnnData instance if name_list == "pca_only": diff --git a/scanpy/external/pp/_mnn_correct.py b/scanpy/external/pp/_mnn_correct.py index 67ff5748ba..ff90cb05be 100644 --- a/scanpy/external/pp/_mnn_correct.py +++ b/scanpy/external/pp/_mnn_correct.py @@ -28,11 +28,7 @@ def mnn_correct( save_raw: bool = False, n_jobs: Optional[int] = None, **kwargs, -) -> Tuple[ - Union[np.ndarray, AnnData], - List[pd.DataFrame], - Optional[List[Tuple[Optional[float], int]]], -]: +) -> Tuple[Union[np.ndarray, AnnData], List[pd.DataFrame], Optional[List[Tuple[Optional[float], int]]],]: """\ Correct batch effects by matching mutual nearest neighbors [Haghverdi18]_ [Kang18]_. @@ -126,10 +122,7 @@ def mnn_correct( try: from mnnpy import mnn_correct except ImportError: - raise ImportError( - 'Please install the package mnnpy ' - '(https://github.com/chriscainx/mnnpy). ' - ) + raise ImportError('Please install the package mnnpy ' '(https://github.com/chriscainx/mnnpy). ') n_jobs = settings.n_jobs if n_jobs is None else n_jobs datas, mnn_list, angle_list = mnn_correct( diff --git a/scanpy/external/pp/_scanorama_integrate.py b/scanpy/external/pp/_scanorama_integrate.py index 9ea4d150cd..0d5a058ec1 100644 --- a/scanpy/external/pp/_scanorama_integrate.py +++ b/scanpy/external/pp/_scanorama_integrate.py @@ -113,9 +113,7 @@ def scanorama_integrate( name2idx[batch_name].append(idx) # Separate batches. - datasets_dimred = [ - adata.obsm[basis][name2idx[batch_name]] for batch_name in batch_names - ] + datasets_dimred = [adata.obsm[basis][name2idx[batch_name]] for batch_name in batch_names] # Integrate. integrated = scanorama.assemble( diff --git a/scanpy/external/pp/_scrublet.py b/scanpy/external/pp/_scrublet.py index 6611fccac5..e2aabb5834 100644 --- a/scanpy/external/pp/_scrublet.py +++ b/scanpy/external/pp/_scrublet.py @@ -150,9 +150,7 @@ def scrublet( try: import scrublet as sl except ImportError: - raise ImportError( - 'Please install scrublet: `pip install scrublet` or `conda install scrublet`.' - ) + raise ImportError('Please install scrublet: `pip install scrublet` or `conda install scrublet`.') if copy: adata = adata.copy() @@ -342,9 +340,7 @@ def _scrublet_call_doublets( try: import scrublet as sl except ImportError: - raise ImportError( - 'Please install scrublet: `pip install scrublet` or `conda install scrublet`.' - ) + raise ImportError('Please install scrublet: `pip install scrublet` or `conda install scrublet`.') # Estimate n_neighbors if not provided, and create scrublet object. @@ -385,14 +381,10 @@ def _scrublet_call_doublets( if mean_center: logg.info('Embedding transcriptomes using PCA...') - sl.pipeline_pca( - scrub, n_prin_comps=n_prin_comps, random_state=scrub.random_state - ) + sl.pipeline_pca(scrub, n_prin_comps=n_prin_comps, random_state=scrub.random_state) else: logg.info('Embedding transcriptomes using Truncated SVD...') - sl.pipeline_truncated_svd( - scrub, n_prin_comps=n_prin_comps, random_state=scrub.random_state - ) + sl.pipeline_truncated_svd(scrub, n_prin_comps=n_prin_comps, random_state=scrub.random_state) # Score the doublets @@ -420,9 +412,7 @@ def _scrublet_call_doublets( 'parameters': { 'expected_doublet_rate': expected_doublet_rate, 'sim_doublet_ratio': ( - adata_sim.uns.get('scrublet', {}) - .get('parameters', {}) - .get('sim_doublet_ratio', None) + adata_sim.uns.get('scrublet', {}).get('parameters', {}).get('sim_doublet_ratio', None) ), 'n_neighbors': n_neighbors, 'random_state': random_state, @@ -430,9 +420,7 @@ def _scrublet_call_doublets( } if get_doublet_neighbor_parents: - adata_obs.uns['scrublet'][ - 'doublet_neighbor_parents' - ] = scrub.doublet_neighbor_parents_ + adata_obs.uns['scrublet']['doublet_neighbor_parents'] = scrub.doublet_neighbor_parents_ return adata_obs @@ -487,9 +475,7 @@ def scrublet_simulate_doublets( try: import scrublet as sl except ImportError: - raise ImportError( - 'Please install scrublet: `pip install scrublet` or `conda install scrublet`.' - ) + raise ImportError('Please install scrublet: `pip install scrublet` or `conda install scrublet`.') X = _get_obs_rep(adata, layer=layer) scrub = sl.Scrublet(X) diff --git a/scanpy/external/pp/_scvi.py b/scanpy/external/pp/_scvi.py index f4f75b0607..724a5634f7 100644 --- a/scanpy/external/pp/_scvi.py +++ b/scanpy/external/pp/_scvi.py @@ -117,9 +117,13 @@ def scvi( from scvi.inference import UnsupervisedTrainer from scvi.dataset import AnnDatasetFromAnnData except ImportError: +<<<<<<< HEAD raise ImportError( "Please install scvi package from https://github.com/YosefLab/scVI" ) +======= + raise ImportError("Please install scvi package from https://github.com/YosefLab/scVI") +>>>>>>> 7a096bf9 (add flake8 pre-commit) # check if observations are unnormalized using first 10 # code from: https://github.com/theislab/dca/blob/89eee4ed01dd969b3d46e0c815382806fbfc2526/dca/io.py#L63-L69 @@ -127,9 +131,13 @@ def scvi( X_subset = adata.X[:10] else: X_subset = adata.X +<<<<<<< HEAD norm_error = ( 'Make sure that the dataset (adata.X) contains unnormalized count data.' ) +======= + norm_error = 'Make sure that the dataset (adata.X) contains unnormalized count data.' +>>>>>>> 7a096bf9 (add flake8 pre-commit) if sp.sparse.issparse(X_subset): assert (X_subset.astype(int) != X_subset).nnz == 0, norm_error else: @@ -185,9 +193,13 @@ def scvi( trainer.train(n_epochs=n_epochs, lr=lr) +<<<<<<< HEAD full = trainer.create_posterior( trainer.model, dataset, indices=np.arange(len(dataset)) ) +======= + full = trainer.create_posterior(trainer.model, dataset, indices=np.arange(len(dataset))) +>>>>>>> 7a096bf9 (add flake8 pre-commit) latent, batch_indices, labels = full.sequential().get_latent() if copy: diff --git a/scanpy/external/tl/_palantir.py b/scanpy/external/tl/_palantir.py index ba437d5f52..8fc05de277 100644 --- a/scanpy/external/tl/_palantir.py +++ b/scanpy/external/tl/_palantir.py @@ -217,9 +217,7 @@ def palantir( # MAGIC imputation if impute_data: - imp_df = run_magic_imputation( - data=adata.to_df(), dm_res=dm_res, n_steps=n_steps - ) + imp_df = run_magic_imputation(data=adata.to_df(), dm_res=dm_res, n_steps=n_steps) adata.layers['palantir_imp'] = imp_df ( diff --git a/scanpy/external/tl/_phate.py b/scanpy/external/tl/_phate.py index 0e077b429e..f4e83d65fe 100644 --- a/scanpy/external/tl/_phate.py +++ b/scanpy/external/tl/_phate.py @@ -130,8 +130,7 @@ def phate( import phate except ImportError: raise ImportError( - 'You need to install the package `phate`: please run `pip install ' - '--user phate` in a terminal.' + 'You need to install the package `phate`: please run `pip install ' '--user phate` in a terminal.' ) X_phate = phate.PHATE( n_components=n_components, diff --git a/scanpy/external/tl/_phenograph.py b/scanpy/external/tl/_phenograph.py index 2a956518a2..ef25c19e20 100644 --- a/scanpy/external/tl/_phenograph.py +++ b/scanpy/external/tl/_phenograph.py @@ -195,10 +195,7 @@ def phenograph( assert phenograph.__version__ >= "1.5.3" except (ImportError, AssertionError, AttributeError): - raise ImportError( - "please install the latest release of phenograph:\n\t" - "pip install -U PhenoGraph" - ) + raise ImportError("please install the latest release of phenograph:\n\t" "pip install -U PhenoGraph") if isinstance(adata, AnnData): try: @@ -209,11 +206,7 @@ def phenograph( data = adata copy = True - comm_key = ( - "pheno_{}".format(clustering_algo) - if clustering_algo in ["louvain", "leiden"] - else '' - ) + comm_key = "pheno_{}".format(clustering_algo) if clustering_algo in ["louvain", "leiden"] else '' ig_key = "pheno_{}_ig".format("jaccard" if jaccard else "gaussian") q_key = "pheno_{}_q".format("jaccard" if jaccard else "gaussian") diff --git a/scanpy/external/tl/_trimap.py b/scanpy/external/tl/_trimap.py index 5334e29fda..89d636e87e 100644 --- a/scanpy/external/tl/_trimap.py +++ b/scanpy/external/tl/_trimap.py @@ -101,8 +101,7 @@ def trimap( X = adata.X if scp.issparse(X): raise ValueError( - 'trimap currently does not support sparse matrices. Please' - 'use a dense matrix or apply pca first.' + 'trimap currently does not support sparse matrices. Please' 'use a dense matrix or apply pca first.' ) logg.warning('`X_pca` not found. Run `sc.pp.pca` first for speedup.') X_trimap = TRIMAP( diff --git a/scanpy/external/tl/_wishbone.py b/scanpy/external/tl/_wishbone.py index 681950a264..c8f3415b58 100644 --- a/scanpy/external/tl/_wishbone.py +++ b/scanpy/external/tl/_wishbone.py @@ -93,24 +93,16 @@ def wishbone( try: from wishbone.core import wishbone as c_wishbone except ImportError: - raise ImportError( - "\nplease install wishbone:\n\n\thttps://github.com/dpeerlab/wishbone" - ) + raise ImportError("\nplease install wishbone:\n\n\thttps://github.com/dpeerlab/wishbone") # Start cell index s = np.where(adata.obs_names == start_cell)[0] if len(s) == 0: - raise RuntimeError( - f"Start cell {start_cell} not found in data. " - "Please rerun with correct start cell." - ) + raise RuntimeError(f"Start cell {start_cell} not found in data. " "Please rerun with correct start cell.") if isinstance(num_waypoints, cabc.Collection): diff = np.setdiff1d(num_waypoints, adata.obs.index) if diff.size > 0: - logging.warning( - "Some of the specified waypoints are not in the data. " - "These will be removed" - ) + logging.warning("Some of the specified waypoints are not in the data. " "These will be removed") num_waypoints = diff.tolist() elif num_waypoints > adata.shape[0]: raise RuntimeError( @@ -132,9 +124,7 @@ def wishbone( # Assign results trajectory = res["Trajectory"] - trajectory = (trajectory - np.min(trajectory)) / ( - np.max(trajectory) - np.min(trajectory) - ) + trajectory = (trajectory - np.min(trajectory)) / (np.max(trajectory) - np.min(trajectory)) adata.obs['trajectory_wishbone'] = np.asarray(trajectory) # branch_ = None @@ -147,9 +137,7 @@ def _anndata_to_wishbone(adata: AnnData): from wishbone.wb import SCData, Wishbone scdata = SCData(adata.to_df()) - scdata.diffusion_eigenvectors = pd.DataFrame( - adata.obsm['X_diffmap'], index=adata.obs_names - ) + scdata.diffusion_eigenvectors = pd.DataFrame(adata.obsm['X_diffmap'], index=adata.obs_names) wb = Wishbone(scdata) wb.trajectory = adata.obs["trajectory_wishbone"] wb.branch = adata.obs["branch_wishbone"] diff --git a/scanpy/get/get.py b/scanpy/get/get.py index 10cde3f9e5..5f1f818dd7 100644 --- a/scanpy/get/get.py +++ b/scanpy/get/get.py @@ -147,26 +147,21 @@ def _check_indices( if key in dim_df.columns: col_keys.append(key) if key in alt_names.index: - raise KeyError( - f"The key '{key}' is found in both adata.{dim} and {alt_repr}.{alt_search_repr}." - ) + raise KeyError(f"The key '{key}' is found in both adata.{dim} and {alt_repr}.{alt_search_repr}.") elif key in alt_names.index: val = alt_names[key] if isinstance(val, pd.Series): # while var_names must be unique, adata.var[gene_symbols] does not # It's still ambiguous to refer to a duplicated entry though. assert alias_index is not None - raise KeyError( - f"Found duplicate entries for '{key}' in {alt_repr}.{alt_search_repr}." - ) + raise KeyError(f"Found duplicate entries for '{key}' in {alt_repr}.{alt_search_repr}.") index_keys.append(val) index_aliases.append(key) else: not_found.append(key) if len(not_found) > 0: raise KeyError( - f"Could not find keys '{not_found}' in columns of `adata.{dim}` or in" - f" {alt_repr}.{alt_search_repr}." + f"Could not find keys '{not_found}' in columns of `adata.{dim}` or in" f" {alt_repr}.{alt_search_repr}." ) return col_keys, index_keys, index_aliases @@ -258,9 +253,7 @@ def obs_df( >>> mean, var = grouped.mean(), grouped.var() """ if use_raw: - assert ( - layer is None - ), "Cannot specify use_raw=True and a layer at the same time." + assert layer is None, "Cannot specify use_raw=True and a layer at the same time." var = adata.raw.var else: var = adata.var @@ -407,8 +400,7 @@ def _get_obs_rep(adata, *, use_raw=False, layer=None, obsm=None, obsp=None): return adata.obsp[obsp] else: assert False, ( - "That was unexpected. Please report this bug at:\n\n\t" - " https://github.com/theislab/scanpy/issues" + "That was unexpected. Please report this bug at:\n\n\t" " https://github.com/theislab/scanpy/issues" ) @@ -434,6 +426,5 @@ def _set_obs_rep(adata, val, *, use_raw=False, layer=None, obsm=None, obsp=None) adata.obsp[obsp] = val else: assert False, ( - "That was unexpected. Please report this bug at:\n\n\t" - " https://github.com/theislab/scanpy/issues" + "That was unexpected. Please report this bug at:\n\n\t" " https://github.com/theislab/scanpy/issues" ) diff --git a/scanpy/logging.py b/scanpy/logging.py index c0f810b281..8da4477542 100644 --- a/scanpy/logging.py +++ b/scanpy/logging.py @@ -84,9 +84,7 @@ def _set_log_level(settings, level: int): class _LogFormatter(logging.Formatter): - def __init__( - self, fmt='{levelname}: {message}', datefmt='%Y-%m-%d %H:%M', style='{' - ): + def __init__(self, fmt='{levelname}: {message}', datefmt='%Y-%m-%d %H:%M', style='{'): super().__init__(fmt, datefmt, style) def format(self, record: logging.LogRecord): @@ -100,13 +98,9 @@ def format(self, record: logging.LogRecord): if record.time_passed: # strip microseconds if record.time_passed.microseconds: - record.time_passed = timedelta( - seconds=int(record.time_passed.total_seconds()) - ) + record.time_passed = timedelta(seconds=int(record.time_passed.total_seconds())) if '{time_passed}' in record.msg: - record.msg = record.msg.replace( - '{time_passed}', str(record.time_passed) - ) + record.msg = record.msg.replace('{time_passed}', str(record.time_passed)) else: self._style._fmt += ' ({time_passed})' if record.deep: diff --git a/scanpy/neighbors/__init__.py b/scanpy/neighbors/__init__.py index 6faf932e20..542eae78c9 100644 --- a/scanpy/neighbors/__init__.py +++ b/scanpy/neighbors/__init__.py @@ -16,16 +16,18 @@ from .. import settings N_DCS = 15 # default number of diffusion components +<<<<<<< HEAD N_PCS = ( settings.N_PCS ) # Backwards compat, constants should be defined in only one place. +======= +N_PCS = settings.N_PCS # Backwards compat, constants should be defined in only one place. +>>>>>>> 7a096bf9 (add flake8 pre-commit) _Method = Literal['umap', 'gauss', 'rapids'] _MetricFn = Callable[[np.ndarray, np.ndarray], float] # from sklearn.metrics.pairwise_distances.__doc__: -_MetricSparseCapable = Literal[ - 'cityblock', 'cosine', 'euclidean', 'l1', 'l2', 'manhattan' -] +_MetricSparseCapable = Literal['cityblock', 'cosine', 'euclidean', 'l1', 'l2', 'manhattan'] _MetricScipySpatial = Literal[ 'braycurtis', 'canberra', @@ -338,9 +340,7 @@ def compute_neighbors_rapids(X: np.ndarray, n_neighbors: int): return knn_indices, np.sqrt(knn_distsq) # cuml uses sqeuclidean metric so take sqrt -def _get_sparse_matrix_from_indices_distances_umap( - knn_indices, knn_dists, n_obs, n_neighbors -): +def _get_sparse_matrix_from_indices_distances_umap(knn_indices, knn_dists, n_obs, n_neighbors): rows = np.zeros((n_obs * n_neighbors), dtype=np.int64) cols = np.zeros((n_obs * n_neighbors), dtype=np.int64) vals = np.zeros((n_obs * n_neighbors), dtype=np.float64) @@ -402,16 +402,12 @@ def _compute_connectivities_umap( # In umap-learn 0.4, this returns (result, sigmas, rhos) connectivities = connectivities[0] - distances = _get_sparse_matrix_from_indices_distances_umap( - knn_indices, knn_dists, n_obs, n_neighbors - ) + distances = _get_sparse_matrix_from_indices_distances_umap(knn_indices, knn_dists, n_obs, n_neighbors) return distances, connectivities.tocsr() -def _get_sparse_matrix_from_indices_distances_numpy( - indices, distances, n_obs, n_neighbors -): +def _get_sparse_matrix_from_indices_distances_numpy(indices, distances, n_obs, n_neighbors): n_nonzero = n_obs * n_neighbors indptr = np.arange(0, n_nonzero + 1, n_neighbors) D = csr_matrix( @@ -440,9 +436,7 @@ def _get_indices_distances_from_sparse_matrix(D, n_neighbors: int): if len(neighbors[1]) > n_neighbors_m1: sorted_indices = np.argsort(D[i][neighbors].A1)[:n_neighbors_m1] indices[i, 1:] = neighbors[1][sorted_indices] - distances[i, 1:] = D[i][ - neighbors[0][sorted_indices], neighbors[1][sorted_indices] - ] + distances[i, 1:] = D[i][neighbors[0][sorted_indices], neighbors[1][sorted_indices]] else: indices[i, 1:] = neighbors[1] distances[i, 1:] = D[i][neighbors] @@ -476,9 +470,7 @@ def _make_forest_dict(forest): props = ('hyperplanes', 'offsets', 'children', 'indices') for prop in props: d[prop] = {} - sizes = np.fromiter( - (getattr(tree, prop).shape[0] for tree in forest), dtype=int - ) + sizes = np.fromiter((getattr(tree, prop).shape[0] for tree in forest), dtype=int) d[prop]['start'] = np.zeros_like(sizes) if prop == 'offsets': dims = sizes.sum() @@ -606,15 +598,9 @@ def count_nonzero(a: Union[np.ndarray, csr_matrix]) -> int: # estimating n_neighbors if self._connectivities is None: - self.n_neighbors = int( - count_nonzero(self._distances) / self._distances.shape[0] - ) + self.n_neighbors = int(count_nonzero(self._distances) / self._distances.shape[0]) else: - self.n_neighbors = int( - count_nonzero(self._connectivities) - / self._connectivities.shape[0] - / 2 - ) + self.n_neighbors = int(count_nonzero(self._connectivities) / self._connectivities.shape[0] / 2) info_str += '`.distances` `.connectivities` ' self._number_connected_components = 1 if issparse(self._connectivities): @@ -629,9 +615,7 @@ def count_nonzero(a: Union[np.ndarray, csr_matrix]) -> int: if n_dcs > len(self._eigen_values): raise ValueError( 'Cannot instantiate using `n_dcs`={}. ' - 'Compute diffmap/spectrum with more components first.'.format( - n_dcs - ) + 'Compute diffmap/spectrum with more components first.'.format(n_dcs) ) self._eigen_values = self._eigen_values[:n_dcs] self._eigen_basis = self._eigen_basis[:, :n_dcs] @@ -756,9 +740,7 @@ def compute_neighbors( if method == 'umap' and not knn: raise ValueError('`method = \'umap\' only with `knn = True`.') if method == 'rapids' and metric != 'euclidean': - raise ValueError( - "`method` 'rapids' only supports the 'euclidean' `metric`." - ) + raise ValueError("`method` 'rapids' only supports the 'euclidean' `metric`.") if method not in {'umap', 'gauss', 'rapids'}: raise ValueError("`method` needs to be 'umap', 'gauss', or 'rapids'.") if self._adata.shape[0] >= 10000 and not knn: @@ -769,14 +751,10 @@ def compute_neighbors( self.knn = knn X = _choose_representation(self._adata, use_rep=use_rep, n_pcs=n_pcs) # neighbor search - use_dense_distances = ( - metric == 'euclidean' and X.shape[0] < 8192 - ) or knn == False + use_dense_distances = (metric == 'euclidean' and X.shape[0] < 8192) or not knn if use_dense_distances: _distances = pairwise_distances(X, metric=metric, **metric_kwds) - knn_indices, knn_distances = _get_indices_distances_from_dense_matrix( - _distances, n_neighbors - ) + knn_indices, knn_distances = _get_indices_distances_from_dense_matrix(_distances, n_neighbors) if knn: self._distances = _get_sparse_matrix_from_indices_distances_numpy( knn_indices, knn_distances, X.shape[0], n_neighbors @@ -829,14 +807,10 @@ def _compute_connectivities_diffmap(self, density_normalize=True): # init distances if self.knn: Dsq = self._distances.power(2) - indices, distances_sq = _get_indices_distances_from_sparse_matrix( - Dsq, self.n_neighbors - ) + indices, distances_sq = _get_indices_distances_from_sparse_matrix(Dsq, self.n_neighbors) else: Dsq = np.power(self._distances, 2) - indices, distances_sq = _get_indices_distances_from_dense_matrix( - Dsq, self.n_neighbors - ) + indices, distances_sq = _get_indices_distances_from_dense_matrix(Dsq, self.n_neighbors) # exclude the first point, the 0th neighbor indices = indices[:, 1:] @@ -862,7 +836,7 @@ def _compute_connectivities_diffmap(self, density_normalize=True): # make the weight matrix sparse if not self.knn: mask = W > 1e-14 - W[mask == False] = 0 + W[not mask] = 0 else: # restrict number of neighbors to ~k # build a symmetric mask @@ -874,11 +848,9 @@ def _compute_connectivities_diffmap(self, density_normalize=True): W[j, i] = W[i, j] mask[j, i] = True # set all entries that are not nearest neighbors to zero - W[mask == False] = 0 + W[not mask] = 0 else: - W = ( - Dsq.copy() - ) # need to copy the distance matrix here; what follows is inplace + W = Dsq.copy() # need to copy the distance matrix here; what follows is inplace for i in range(len(Dsq.indptr[:-1])): row = Dsq.indices[Dsq.indptr[i] : Dsq.indptr[i + 1]] num = 2 * sigmas[i] * sigmas[row] @@ -980,17 +952,12 @@ def compute_eigen( which = 'LM' if sort == 'decrease' else 'SM' # it pays off to increase the stability with a bit more precision matrix = matrix.astype(np.float64) - evals, evecs = scipy.sparse.linalg.eigsh( - matrix, k=n_comps, which=which, ncv=ncv - ) + evals, evecs = scipy.sparse.linalg.eigsh(matrix, k=n_comps, which=which, ncv=ncv) evals, evecs = evals.astype(np.float32), evecs.astype(np.float32) if sort == 'decrease': evals = evals[::-1] evecs = evecs[:, ::-1] - logg.info( - ' eigenvalues of transition matrix\n' - ' {}'.format(str(evals).replace('\n', '\n ')) - ) + logg.info(' eigenvalues of transition matrix\n' ' {}'.format(str(evals).replace('\n', '\n '))) if self._number_connected_components > len(evals) / 2: logg.warning('Transition matrix has many disconnected components!') self._eigen_values = evals @@ -1024,15 +991,11 @@ def _get_dpt_row(self, i): label = self._connected_components[1][i] mask = self._connected_components[1] == label row = sum( - ( - self.eigen_values[l] - / (1 - self.eigen_values[l]) - * (self.eigen_basis[i, l] - self.eigen_basis[:, l]) - ) - ** 2 + (self.eigen_values[i] / (1 - self.eigen_values[i]) * (self.eigen_basis[i, i] - self.eigen_basis[:, i])) + ** 2 # noqa: E126 # account for float32 precision - for l in range(0, self.eigen_values.size) - if self.eigen_values[l] < 0.9994 + for i in range(0, self.eigen_values.size) + if self.eigen_values[i] < 0.9994 ) # thanks to Marius Lange for pointing Alex to this: # we will likely remove the contributions from the stationary state below when making @@ -1040,9 +1003,9 @@ def _get_dpt_row(self, i): # they never seem to have deteriorated results, but also other distance measures (see e.g. # PAGA paper) don't have it, which makes sense row += sum( - (self.eigen_basis[i, l] - self.eigen_basis[:, l]) ** 2 - for l in range(0, self.eigen_values.size) - if self.eigen_values[l] >= 0.9994 + (self.eigen_basis[i, j] - self.eigen_basis[:, j]) ** 2 + for j in range(0, self.eigen_values.size) + if self.eigen_values[j] >= 0.9994 ) if mask is not None: row[~mask] = np.inf @@ -1066,9 +1029,7 @@ def _set_iroot_via_xroot(self, xroot): condition, only relevant for computing pseudotime. """ if self._adata.shape[1] != xroot.size: - raise ValueError( - 'The root vector you provided does not have the ' 'correct dimension.' - ) + raise ValueError('The root vector you provided does not have the ' 'correct dimension.') # this is the squared distance dsqroot = 1e10 iroot = 0 diff --git a/scanpy/plotting/_anndata.py b/scanpy/plotting/_anndata.py index 6c319682ce..af5754ac72 100755 --- a/scanpy/plotting/_anndata.py +++ b/scanpy/plotting/_anndata.py @@ -144,10 +144,7 @@ def scatter( # store .uns annotations that were added to the new adata object adata.uns = adata_T.uns return axs - raise ValueError( - '`x`, `y`, and potential `color` inputs must all ' - 'come from either `.obs` or `.var`' - ) + raise ValueError('`x`, `y`, and potential `color` inputs must all ' 'come from either `.obs` or `.var`') def _scatter_obs( @@ -185,43 +182,29 @@ def _scatter_obs( use_raw = _check_use_raw(adata, use_raw) # Process layers - if layers in ['X', None] or ( - isinstance(layers, str) and layers in adata.layers.keys() - ): + if layers in ['X', None] or (isinstance(layers, str) and layers in adata.layers.keys()): layers = (layers, layers, layers) elif isinstance(layers, cabc.Collection) and len(layers) == 3: layers = tuple(layers) for layer in layers: if layer not in adata.layers.keys() and layer not in ['X', None]: - raise ValueError( - '`layers` should have elements that are ' - 'either None or in adata.layers.keys().' - ) + raise ValueError('`layers` should have elements that are ' 'either None or in adata.layers.keys().') else: raise ValueError( - "`layers` should be a string or a collection of strings " - f"with length 3, had value '{layers}'" + "`layers` should be a string or a collection of strings " f"with length 3, had value '{layers}'" ) if use_raw and layers not in [('X', 'X', 'X'), (None, None, None)]: ValueError('`use_raw` must be `False` if layers are used.') if legend_loc not in VALID_LEGENDLOCS: - raise ValueError( - f'Invalid `legend_loc`, need to be one of: {VALID_LEGENDLOCS}.' - ) + raise ValueError(f'Invalid `legend_loc`, need to be one of: {VALID_LEGENDLOCS}.') if components is None: components = '1,2' if '2d' in projection else '1,2,3' if isinstance(components, str): components = components.split(',') components = np.array(components).astype(int) - 1 # color can be a obs column name or a matplotlib color specification - keys = ( - ['grey'] - if color is None - else [color] - if isinstance(color, str) or is_color_like(color) - else color - ) + keys = ['grey'] if color is None else [color] if isinstance(color, str) or is_color_like(color) else color if title is not None and isinstance(title, str): title = [title] highlights = adata.uns['highlights'] if 'highlights' in adata.uns else [] @@ -235,9 +218,7 @@ def _scatter_obs( if basis == 'diffmap': components -= 1 except KeyError: - raise KeyError( - f'compute coordinates using visualization tool {basis} first' - ) + raise KeyError(f'compute coordinates using visualization tool {basis} first') elif x is not None and y is not None: if use_raw: if x in adata.obs.columns: @@ -337,9 +318,7 @@ def _scatter_obs( if legend_loc == 'right margin': right_margin = 0.5 if title is None and keys[0] is not None: - title = [ - key.replace('_', ' ') if not is_color_like(key) else '' for key in keys - ] + title = [key.replace('_', ' ') if not is_color_like(key) else '' for key in keys] axs = scatter_base( Y, @@ -399,13 +378,10 @@ def add_centroid(centroids, name, Y, mask): for name in groups: if name not in set(adata.obs[key].cat.categories): raise ValueError( - f'{name!r} is invalid! specify valid name, ' - f'one of {adata.obs[key].cat.categories}' + f'{name!r} is invalid! specify valid name, ' f'one of {adata.obs[key].cat.categories}' ) else: - iname = np.flatnonzero( - adata.obs[key].cat.categories.values == name - )[0] + iname = np.flatnonzero(adata.obs[key].cat.categories.values == name)[0] mask = scatter_group( axs[ikey], key, @@ -436,9 +412,7 @@ def add_centroid(centroids, name, Y, mask): if legend_fontweight is None: legend_fontweight = 'bold' if legend_fontoutline is not None: - path_effect = [ - patheffects.withStroke(linewidth=legend_fontoutline, foreground='w') - ] + path_effect = [patheffects.withStroke(linewidth=legend_fontoutline, foreground='w')] else: path_effect = None for name, pos in centroids.items(): @@ -480,9 +454,7 @@ def add_centroid(centroids, name, Y, mask): fontsize=legend_fontsize, ) elif legend_loc != 'none': - legend = axs[ikey].legend( - frameon=False, loc=legend_loc, fontsize=legend_fontsize - ) + legend = axs[ikey].legend(frameon=False, loc=legend_loc, fontsize=legend_fontsize) if legend is not None: for handle in legend.legendHandles: handle.set_sizes([300.0]) @@ -546,11 +518,7 @@ def ranking( if log: scores = np.log(scores) if labels is None: - labels = ( - adata.var_names - if attr in {'var', 'varm'} - else np.arange(scores.shape[0]).astype(str) - ) + labels = adata.var_names if attr in {'var', 'varm'} else np.arange(scores.shape[0]).astype(str) if isinstance(labels, str): labels = [labels + str(i + 1) for i in range(scores.shape[0])] if n_panels <= 5: @@ -702,14 +670,9 @@ def violin( ylabel = [ylabel] * (1 if groupby is None else len(keys)) if groupby is None: if len(ylabel) != 1: - raise ValueError( - f'Expected number of y-labels to be `1`, found `{len(ylabel)}`.' - ) + raise ValueError(f'Expected number of y-labels to be `1`, found `{len(ylabel)}`.') elif len(ylabel) != len(keys): - raise ValueError( - f'Expected number of y-labels to be `{len(keys)}`, ' - f'found `{len(ylabel)}`.' - ) + raise ValueError(f'Expected number of y-labels to be `{len(keys)}`, ' f'found `{len(ylabel)}`.') if groupby is not None: obs_df = get.obs_df(adata, keys=[groupby] + keys, layer=layer, use_raw=use_raw) @@ -720,9 +683,7 @@ def violin( f'but is of dtype {adata.obs[groupby].dtype}.' ) _utils.add_colors_for_categorical_sample_annotation(adata, groupby) - kwds['palette'] = dict( - zip(obs_df[groupby].cat.categories, adata.uns[f'{groupby}_colors']) - ) + kwds['palette'] = dict(zip(obs_df[groupby].cat.categories, adata.uns[f'{groupby}_colors'])) else: obs_df = get.obs_df(adata, keys=keys, layer=layer, use_raw=use_raw) if groupby is None: @@ -1054,9 +1015,7 @@ def heatmap( # reorder groupby colors if groupby_colors is not None: - groupby_colors = [ - groupby_colors[x] for x in dendro_data['categories_idx_ordered'] - ] + groupby_colors = [groupby_colors[x] for x in dendro_data['categories_idx_ordered']] if show_gene_labels is None: if len(var_names) <= 50: @@ -1146,14 +1105,10 @@ def heatmap( labels, groupby_cmap, norm, - ) = _plot_categories_as_colorblocks( - groupby_ax, obs_tidy, colors=groupby_colors, orientation='left' - ) + ) = _plot_categories_as_colorblocks(groupby_ax, obs_tidy, colors=groupby_colors, orientation='left') # add lines to main heatmap - line_positions = ( - np.cumsum(obs_tidy.index.value_counts(sort=False))[:-1] - 0.5 - ) + line_positions = np.cumsum(obs_tidy.index.value_counts(sort=False))[:-1] - 0.5 heatmap_ax.hlines( line_positions, -0.5, @@ -1166,9 +1121,7 @@ def heatmap( if dendrogram: dendro_ax = fig.add_subplot(axs[1, 2], sharey=heatmap_ax) - _plot_dendrogram( - dendro_ax, adata, groupby, ticks=ticks, dendrogram_key=dendrogram - ) + _plot_dendrogram(dendro_ax, adata, groupby, ticks=ticks, dendrogram_key=dendrogram) # plot group legends on top of heatmap_ax (if given) if var_group_positions is not None and len(var_group_positions) > 0: @@ -1247,13 +1200,9 @@ def heatmap( labels, groupby_cmap, norm, - ) = _plot_categories_as_colorblocks( - groupby_ax, obs_tidy, colors=groupby_colors, orientation='bottom' - ) + ) = _plot_categories_as_colorblocks(groupby_ax, obs_tidy, colors=groupby_colors, orientation='bottom') # add lines to main heatmap - line_positions = ( - np.cumsum(obs_tidy.index.value_counts(sort=False))[:-1] - 0.5 - ) + line_positions = np.cumsum(obs_tidy.index.value_counts(sort=False))[:-1] - 0.5 heatmap_ax.vlines( line_positions, -0.5, @@ -1279,17 +1228,13 @@ def heatmap( if var_group_positions is not None and len(var_group_positions) > 0: gene_groups_ax = fig.add_subplot(axs[1, 1]) arr = [] - for idx, (label, pos) in enumerate( - zip(var_group_labels, var_group_positions) - ): + for idx, (label, pos) in enumerate(zip(var_group_labels, var_group_positions)): if var_groups_subset_of_groupby: label_code = label2code[label] else: label_code = idx arr += [label_code] * (pos[1] + 1 - pos[0]) - gene_groups_ax.imshow( - np.array([arr]).T, aspect='auto', cmap=groupby_cmap, norm=norm - ) + gene_groups_ax.imshow(np.array([arr]).T, aspect='auto', cmap=groupby_cmap, norm=norm) gene_groups_ax.axis('off') # plot colorbar @@ -1416,9 +1361,7 @@ def tracksplot( ) categories = [categories[x] for x in dendro_data['categories_idx_ordered']] - groupby_colors = [ - groupby_colors[x] for x in dendro_data['categories_idx_ordered'] - ] + groupby_colors = [groupby_colors[x] for x in dendro_data['categories_idx_ordered']] obs_tidy = obs_tidy.sort_index() @@ -1509,9 +1452,7 @@ def tracksplot( # the ax to plot the groupby categories is split to add a small space # between the rest of the plot and the categories - axs2 = gridspec.GridSpecFromSubplotSpec( - 2, 1, subplot_spec=axs[num_rows - 1, 0], height_ratios=[1, 1] - ) + axs2 = gridspec.GridSpecFromSubplotSpec(2, 1, subplot_spec=axs[num_rows - 1, 0], height_ratios=[1, 1]) groupby_ax = fig.add_subplot(axs2[1]) @@ -1542,9 +1483,7 @@ def tracksplot( for idx, pos in enumerate(var_group_positions): arr += [idx] * (pos[1] + 1 - pos[0]) - gene_groups_ax.imshow( - np.array([arr]).T, aspect='auto', cmap=groupby_cmap, norm=norm - ) + gene_groups_ax.imshow(np.array([arr]).T, aspect='auto', cmap=groupby_cmap, norm=norm) gene_groups_ax.axis('off') return_ax_dict = {'track_axes': axs_list, 'groupby_ax': groupby_ax} @@ -1851,10 +1790,7 @@ def _prepare_dataframe( f'Given {group}, is not in observations: {adata.obs_keys()}' + msg ) if group in adata.obs.keys() and group == adata.obs.index.name: - raise ValueError( - f'Given group {group} is both and index and a column level, ' - 'which is ambiguous.' - ) + raise ValueError(f'Given group {group} is both and index and a column level, ' 'which is ambiguous.') if group == adata.obs.index.name: groupby_index = group if groupby_index is not None: @@ -1863,9 +1799,7 @@ def _prepare_dataframe( groupby = groupby.copy() # copy to not modify user passed parameter groupby.remove(groupby_index) keys = list(groupby) + list(np.unique(var_names)) - obs_tidy = get.obs_df( - adata, keys=keys, layer=layer, use_raw=use_raw, gene_symbols=gene_symbols - ) + obs_tidy = get.obs_df(adata, keys=keys, layer=layer, use_raw=use_raw, gene_symbols=gene_symbols) assert np.all(np.array(keys) == np.array(obs_tidy.columns)) if groupby_index is not None: @@ -2038,9 +1972,7 @@ def _plot_gene_groups_brackets( # remove y ticks gene_groups_ax.tick_params(axis='y', left=False, labelleft=False) # remove x ticks and labels - gene_groups_ax.tick_params( - axis='x', bottom=False, labelbottom=False, labeltop=False - ) + gene_groups_ax.tick_params(axis='x', bottom=False, labelbottom=False, labeltop=False) def _reorder_categories_after_dendrogram( @@ -2116,9 +2048,7 @@ def _reorder_categories_after_dendrogram( position = var_group_positions[idx] _var_names = var_names[position[0] : position[1] + 1] var_names_idx_ordered.extend(range(position[0], position[1] + 1)) - positions_ordered.append( - (position_start, position_start + len(_var_names) - 1) - ) + positions_ordered.append((position_start, position_start + len(_var_names) - 1)) position_start += len(_var_names) labels_ordered.append(var_group_labels[idx]) var_group_labels = labels_ordered @@ -2176,8 +2106,7 @@ def _get_dendrogram_key(adata, dendrogram_key, groupby): if 'dendrogram_info' not in adata.uns[dendrogram_key]: raise ValueError( - f"The given dendrogram key ({dendrogram_key!r}) does not contain " - "valid dendrogram information." + f"The given dendrogram key ({dendrogram_key!r}) does not contain " "valid dendrogram information." ) return dendrogram_key @@ -2249,9 +2178,7 @@ def translate_pos(pos_list, new_ticks, old_ticks): old_max = old_ticks[idx_next] new_min = new_ticks[idx_prev] new_max = new_ticks[idx_next] - new_x_val = ((x_val - old_min) / (old_max - old_min)) * ( - new_max - new_min - ) + new_min + new_x_val = ((x_val - old_min) / (old_max - old_min)) * (new_max - new_min) + new_min new_xs.append(new_x_val) return new_xs @@ -2263,10 +2190,7 @@ def translate_pos(pos_list, new_ticks, old_ticks): orig_ticks = np.arange(5, len(leaves) * 10 + 5, 10).astype(float) # check that ticks has the same length as orig_ticks if ticks is not None and len(orig_ticks) != len(ticks): - logg.warning( - "ticks argument does not have the same size as orig_ticks. " - "The argument will be ignored" - ) + logg.warning("ticks argument does not have the same size as orig_ticks. " "The argument will be ignored") ticks = None for xs, ys in zip(icoord, dcoord): @@ -2296,9 +2220,7 @@ def translate_pos(pos_list, new_ticks, old_ticks): dendro_ax.tick_params(labeltop=True, labelbottom=False) if remove_labels: - dendro_ax.tick_params( - labelbottom=False, labeltop=False, labelleft=False, labelright=False - ) + dendro_ax.tick_params(labelbottom=False, labeltop=False, labelleft=False, labelright=False) dendro_ax.grid(False) @@ -2349,9 +2271,7 @@ def _plot_categories_as_colorblocks( ticks = [] # list of centered position of the labels labels = [] label2code = {} # dictionary of numerical values asigned to each label - for code, (label, value) in enumerate( - obs_tidy.index.value_counts(sort=False).iteritems() - ): + for code, (label, value) in enumerate(obs_tidy.index.value_counts(sort=False).iteritems()): ticks.append(value_sum + (value / 2)) labels.append(label) value_sum += value diff --git a/scanpy/plotting/_baseplot_class.py b/scanpy/plotting/_baseplot_class.py index e28a535fed..2748348807 100644 --- a/scanpy/plotting/_baseplot_class.py +++ b/scanpy/plotting/_baseplot_class.py @@ -100,11 +100,7 @@ def __init__( self.var_group_rotation = var_group_rotation self.width, self.height = figsize if figsize is not None else (None, None) - self.has_var_groups = ( - True - if var_group_positions is not None and len(var_group_positions) > 0 - else False - ) + self.has_var_groups = True if var_group_positions is not None and len(var_group_positions) > 0 else False self._update_var_groups() @@ -119,15 +115,12 @@ def __init__( gene_symbols=gene_symbols, ) if len(self.categories) > self.MAX_NUM_CATEGORIES: - warn( - f"Over {self.MAX_NUM_CATEGORIES} categories found. " - "Plot would be very large." - ) + warn(f"Over {self.MAX_NUM_CATEGORIES} categories found. " "Plot would be very large.") if categories_order is not None: if set(self.obs_tidy.index.categories) != set(categories_order): logg.error( - "Please check that the categories given by " + "Please check that the categories given by " # noqa: F821 "the `order` parameter match the categories that " "want to be reordered.\n\n" "Mismatch: " @@ -256,10 +249,7 @@ def add_dendrogram( if self.groupby is None or len(self.categories) <= 2: # dendrogram can only be computed between groupby categories - logg.warning( - "Dendrogram not added. Dendrogram is added only " - "when the number of categories to plot > 2" - ) + logg.warning("Dendrogram not added. Dendrogram is added only " "when the number of categories to plot > 2") return self self.group_extra_size = size @@ -410,9 +400,7 @@ def get_axes(self): self.make_figure() return self.ax_dict - def _plot_totals( - self, total_barplot_ax: Axes, orientation: Literal['top', 'right'] - ): + def _plot_totals(self, total_barplot_ax: Axes, orientation: Literal['top', 'right']): """ Makes the bar plot for totals """ @@ -507,9 +495,7 @@ def _plot_colorbar(self, color_legend_ax: Axes, normalize): cmap = pl.get_cmap(self.cmap) import matplotlib.colorbar - matplotlib.colorbar.ColorbarBase( - color_legend_ax, orientation='horizontal', cmap=cmap, norm=normalize - ) + matplotlib.colorbar.ColorbarBase(color_legend_ax, orientation='horizontal', cmap=cmap, norm=normalize) color_legend_ax.set_title(self.color_legend_title, fontsize='small') @@ -528,9 +514,7 @@ def _plot_legend(self, legend_ax, return_ax_dict, normalize): self.height - legend_height, legend_height, ] - fig, legend_gs = make_grid_spec( - legend_ax, nrows=2, ncols=1, height_ratios=height_ratios - ) + fig, legend_gs = make_grid_spec(legend_ax, nrows=2, ncols=1, height_ratios=height_ratios) color_legend_ax = fig.add_subplot(legend_gs[1]) @@ -604,9 +588,7 @@ def make_figure(self): if self.height is None: mainplot_height = len(self.categories) * category_height - mainplot_width = ( - len(self.var_names) * category_width + self.group_extra_size - ) + mainplot_width = len(self.var_names) * category_width + self.group_extra_size if self.are_axes_swapped: mainplot_height, mainplot_width = mainplot_width, mainplot_height @@ -871,9 +853,7 @@ def _format_first_three_categories(_categories): position = self.var_group_positions[idx] _var_names = self.var_names[position[0] : position[1] + 1] var_names_idx_ordered.extend(range(position[0], position[1] + 1)) - positions_ordered.append( - (position_start, position_start + len(_var_names) - 1) - ) + positions_ordered.append((position_start, position_start + len(_var_names) - 1)) position_start += len(_var_names) labels_ordered.append(self.var_group_labels[idx]) self.var_group_labels = labels_ordered @@ -1019,9 +999,7 @@ def _plot_var_groups_brackets( # remove y ticks gene_groups_ax.tick_params(axis='y', left=False, labelleft=False) # remove x ticks and labels - gene_groups_ax.tick_params( - axis='x', bottom=False, labelbottom=False, labeltop=False - ) + gene_groups_ax.tick_params(axis='x', bottom=False, labelbottom=False, labeltop=False) def _update_var_groups(self): """ diff --git a/scanpy/plotting/_dotplot.py b/scanpy/plotting/_dotplot.py index de1ef06c41..3a9856c308 100644 --- a/scanpy/plotting/_dotplot.py +++ b/scanpy/plotting/_dotplot.py @@ -165,16 +165,12 @@ def __init__( # of values >expression_cutoff, and divide the result by the total number of # values in the group (given by `count()`) if dot_size_df is None: - dot_size_df = ( - obs_bool.groupby(level=0).sum() / obs_bool.groupby(level=0).count() - ) + dot_size_df = obs_bool.groupby(level=0).sum() / obs_bool.groupby(level=0).count() if dot_color_df is None: # 2. compute mean expression value value if mean_only_expressed: - dot_color_df = ( - self.obs_tidy.mask(~obs_bool).groupby(level=0).mean().fillna(0) - ) + dot_color_df = self.obs_tidy.mask(~obs_bool).groupby(level=0).mean().fillna(0) else: dot_color_df = self.obs_tidy.groupby(level=0).mean() @@ -205,9 +201,7 @@ def __init__( # with df[['a', 'a', 'b']], results in a df with columns: # ['a', 'a', 'a', 'a', 'b'] - unique_var_names, unique_idx = np.unique( - dot_color_df.columns, return_index=True - ) + unique_var_names, unique_idx = np.unique(dot_color_df.columns, return_index=True) # remove duplicate columns if len(unique_var_names) != len(self.var_names): dot_color_df = dot_color_df.iloc[:, unique_idx] @@ -447,15 +441,11 @@ def _plot_size_legend(self, size_legend_ax: Axes): zorder=100, ) size_legend_ax.set_xticks(np.arange(len(size)) + 0.5) - labels = [ - "{}".format(np.round((x * 100), decimals=0).astype(int)) for x in size_range - ] + labels = ["{}".format(np.round((x * 100), decimals=0).astype(int)) for x in size_range] size_legend_ax.set_xticklabels(labels, fontsize='small') # remove y ticks and labels - size_legend_ax.tick_params( - axis='y', left=False, labelleft=False, labelright=False - ) + size_legend_ax.tick_params(axis='y', left=False, labelleft=False, labelright=False) # remove surrounding lines size_legend_ax.spines['right'].set_visible(False) @@ -492,9 +482,7 @@ def _plot_legend(self, legend_ax, return_ax_dict, normalize): spacer_height, cbar_legend_height, ] - fig, legend_gs = make_grid_spec( - legend_ax, nrows=4, ncols=1, height_ratios=height_ratios - ) + fig, legend_gs = make_grid_spec(legend_ax, nrows=4, ncols=1, height_ratios=height_ratios) if self.show_size_legend: size_legend_ax = fig.add_subplot(legend_gs[1]) @@ -648,8 +636,7 @@ def _dotplot( ) assert list(dot_size.columns) == list(dot_color.columns), ( - 'please check that the dot_size ' - 'and dot_color dataframes have the same columns' + 'please check that the dot_size ' 'and dot_color dataframes have the same columns' ) if standard_scale == 'group': @@ -750,9 +737,7 @@ def _dotplot( y_ticks = np.arange(dot_color.shape[0]) + 0.5 dot_ax.set_yticks(y_ticks) - dot_ax.set_yticklabels( - [dot_color.index[idx] for idx, _ in enumerate(y_ticks)], minor=False - ) + dot_ax.set_yticklabels([dot_color.index[idx] for idx, _ in enumerate(y_ticks)], minor=False) x_ticks = np.arange(dot_color.shape[1]) + 0.5 dot_ax.set_xticks(x_ticks) diff --git a/scanpy/plotting/_preprocessing.py b/scanpy/plotting/_preprocessing.py index 9837773334..ad9e64d27b 100644 --- a/scanpy/plotting/_preprocessing.py +++ b/scanpy/plotting/_preprocessing.py @@ -120,6 +120,4 @@ def filter_genes_dispersion( A string is appended to the default filename. Infer the filetype if ending on {{`'.pdf'`, `'.png'`, `'.svg'`}}. """ - highly_variable_genes( - result, log=log, show=show, save=save, highly_variable_genes=False - ) + highly_variable_genes(result, log=log, show=show, save=save, highly_variable_genes=False) diff --git a/scanpy/plotting/_qc.py b/scanpy/plotting/_qc.py index 3259e66425..d33d5fec2a 100644 --- a/scanpy/plotting/_qc.py +++ b/scanpy/plotting/_qc.py @@ -75,14 +75,8 @@ def highest_expr_genes( mean_percent = norm_dict['X'].mean(axis=0) top_idx = np.argsort(mean_percent)[::-1][:n_top] counts_top_genes = norm_dict['X'][:, top_idx] - columns = ( - adata.var_names[top_idx] - if gene_symbols is None - else adata.var[gene_symbols][top_idx] - ) - counts_top_genes = pd.DataFrame( - counts_top_genes, index=adata.obs_names, columns=columns - ) + columns = adata.var_names[top_idx] if gene_symbols is None else adata.var[gene_symbols][top_idx] + counts_top_genes = pd.DataFrame(counts_top_genes, index=adata.obs_names, columns=columns) if not ax: # figsize is hardcoded to produce a tall image. To change the fig size, diff --git a/scanpy/plotting/_stacked_violin.py b/scanpy/plotting/_stacked_violin.py index 347c8cc569..d291e6e4d9 100644 --- a/scanpy/plotting/_stacked_violin.py +++ b/scanpy/plotting/_stacked_violin.py @@ -316,9 +316,7 @@ def _mainplot(self, ax): _matrix = _matrix.iloc[:, self.var_names_idx_order] if self.categories_order is not None: - _matrix.index = _matrix.index.reorder_categories( - self.categories_order, ordered=True - ) + _matrix.index = _matrix.index.reorder_categories(self.categories_order, ordered=True) # get mean values for color and transform to color values # using colormap @@ -338,9 +336,7 @@ def _mainplot(self, ax): colormap_array = cmap(normalize(_color_df.values)) x_spacer_size = self.plot_x_padding y_spacer_size = self.plot_y_padding - self._make_rows_of_violinplots( - ax, _matrix, colormap_array, _color_df, x_spacer_size, y_spacer_size - ) + self._make_rows_of_violinplots(ax, _matrix, colormap_array, _color_df, x_spacer_size, y_spacer_size) # turn on axis for `ax` as this is turned off # by make_grid_spec when the axis is subdivided earlier. @@ -355,9 +351,7 @@ def _mainplot(self, ax): # 0.5 to position the ticks on the center of the violins y_ticks = np.arange(_color_df.shape[0]) + 0.5 ax.set_yticks(y_ticks) - ax.set_yticklabels( - [_color_df.index[idx] for idx, _ in enumerate(y_ticks)], minor=False - ) + ax.set_yticklabels([_color_df.index[idx] for idx, _ in enumerate(y_ticks)], minor=False) # 0.5 to position the ticks on the center of the violins x_ticks = np.arange(_color_df.shape[1]) + 0.5 @@ -372,9 +366,7 @@ def _mainplot(self, ax): return normalize - def _make_rows_of_violinplots( - self, ax, _matrix, colormap_array, _color_df, x_spacer_size, y_spacer_size - ): + def _make_rows_of_violinplots(self, ax, _matrix, colormap_array, _color_df, x_spacer_size, y_spacer_size): import seaborn as sns # Slow import, only import if called row_palette = self.kwds.get('color', self.row_palette) @@ -413,14 +405,8 @@ def _make_rows_of_violinplots( } ) ) - df['genes'] = ( - df['genes'].astype('category').cat.reorder_categories(_matrix.columns) - ) - df['categories'] = ( - df['categories'] - .astype('category') - .cat.reorder_categories(_matrix.index.categories) - ) + df['genes'] = df['genes'].astype('category').cat.reorder_categories(_matrix.columns) + df['categories'] = df['categories'].astype('category').cat.reorder_categories(_matrix.index.categories) # the ax need to be subdivided # define a layout of nrows = len(categories) rows @@ -522,9 +508,7 @@ def _setup_violin_axes_ticks(self, row_ax, num_cols): import matplotlib.ticker as ticker # use MaxNLocator to set 2 ticks - row_ax.yaxis.set_major_locator( - ticker.MaxNLocator(nbins=2, steps=[1, 1.2, 10]) - ) + row_ax.yaxis.set_major_locator(ticker.MaxNLocator(nbins=2, steps=[1, 1.2, 10])) yticks = row_ax.get_yticks() row_ax.set_yticks([yticks[0], yticks[-1]]) ticklabels = row_ax.get_yticklabels() @@ -541,9 +525,7 @@ def _setup_violin_axes_ticks(self, row_ax, num_cols): row_ax.set_xlabel('') row_ax.set_xticklabels([]) - row_ax.tick_params( - axis='x', bottom=False, top=False, labeltop=False, labelbottom=False - ) + row_ax.tick_params(axis='x', bottom=False, top=False, labeltop=False, labelbottom=False) @_doc_params( diff --git a/scanpy/plotting/_tools/__init__.py b/scanpy/plotting/_tools/__init__.py index 42e4a7da88..d1e6f2e63b 100644 --- a/scanpy/plotting/_tools/__init__.py +++ b/scanpy/plotting/_tools/__init__.py @@ -62,7 +62,11 @@ def pca_overview(adata: AnnData, **params): show = params['show'] if 'show' in params else None if 'show' in params: del params['show'] +<<<<<<< HEAD pca(adata, **params, show=False) +======= + scatterplots.pca(adata, **params, show=False) # noqa: F821 +>>>>>>> 7a096bf9 (add flake8 pre-commit) pca_loadings(adata, show=False) pca_variance_ratio(adata, show=show) @@ -399,14 +403,10 @@ def _rank_genes_groups_plot( if min_logfoldchange is not None: df = rank_genes_groups_df(adata, group, key=key) # select genes with given log_fold change - genes_list = df[df.logfoldchanges > min_logfoldchange].names.tolist()[ - :n_genes - ] + genes_list = df[df.logfoldchanges > min_logfoldchange].names.tolist()[:n_genes] else: # get all genes that are 'non-nan' - genes_list = [ - gene for gene in adata.uns[key]['names'][group] if not pd.isnull(gene) - ][:n_genes] + genes_list = [gene for gene in adata.uns[key]['names'][group] if not pd.isnull(gene)][:n_genes] if len(genes_list) == 0: logg.warning(f'No genes found for group {group}') @@ -447,9 +447,7 @@ def _rank_genes_groups_plot( elif plot_type == 'matrixplot': from .._matrixplot import matrixplot - _pl = matrixplot( - adata, var_names, groupby, values_df=values_df, return_fig=True, **kwds - ) + _pl = matrixplot(adata, var_names, groupby, values_df=values_df, return_fig=True, **kwds) if title is not None and 'colorbar_title' not in kwds: _pl.legend(title=title) @@ -960,11 +958,7 @@ def rank_genes_groups_violin( _ax.legend_.remove() _ax.set_ylabel('expression') _ax.set_xticklabels(new_gene_names, rotation='vertical') - writekey = ( - f"rank_genes_groups_" - f"{adata.uns[key]['params']['groupby']}_" - f"{group_name}" - ) + writekey = f"rank_genes_groups_" f"{adata.uns[key]['params']['groupby']}_" f"{group_name}" savefig_or_show(writekey, show=show, save=save) axs.append(_ax) if not show: @@ -1157,15 +1151,11 @@ def embedding_density( if f'X_{basis}' not in adata.obsm_keys(): raise ValueError( - f'Cannot find the embedded representation `adata.obsm[X_{basis!r}]`. ' - 'Compute the embedding first.' + f'Cannot find the embedded representation `adata.obsm[X_{basis!r}]`. ' 'Compute the embedding first.' ) if key not in adata.obs or f'{key}_params' not in adata.uns: - raise ValueError( - 'Please run `sc.tl.embedding_density()` first ' - 'and specify the correct key.' - ) + raise ValueError('Please run `sc.tl.embedding_density()` first ' 'and specify the correct key.') if 'components' in kwargs: logg.warning( @@ -1188,15 +1178,11 @@ def embedding_density( if group is None and groupby is not None: raise ValueError( - 'Densities were calculated over an `.obs` covariate. ' - 'Please specify a group from this covariate to plot.' + 'Densities were calculated over an `.obs` covariate. ' 'Please specify a group from this covariate to plot.' ) if group is not None and groupby is None: - logg.warning( - "value of 'group' is ignored because densities " - "were not calculated for an `.obs` covariate." - ) + logg.warning("value of 'group' is ignored because densities " "were not calculated for an `.obs` covariate.") group = None if np.min(adata.obs[key]) < 0 or np.max(adata.obs[key]) > 1: @@ -1223,11 +1209,7 @@ def embedding_density( # if group is set, then plot it using multiple panels # (even if only one group is set) - if ( - group is not None - and not isinstance(group, str) - and isinstance(group, cabc.Sequence) - ): + if group is not None and not isinstance(group, str) and isinstance(group, cabc.Sequence): if ax is not None: raise ValueError("Can only specify `ax` if no `group` sequence is given.") fig, gs = _panel_grid(hspace, wspace, ncols, len(group)) @@ -1387,9 +1369,7 @@ def _get_values_to_plot( column = values_to_plot.replace('log10_', '') else: column = values_to_plot - values_df = pd.pivot( - values_df, index='names', columns='group', values=column - ).fillna(1) + values_df = pd.pivot(values_df, index='names', columns='group', values=column).fillna(1) if values_to_plot in ['log10_pvals', 'log10_pvals_adj']: values_df = -1 * np.log10(values_df) diff --git a/scanpy/plotting/_tools/paga.py b/scanpy/plotting/_tools/paga.py index 4f565b18ba..56960717d0 100644 --- a/scanpy/plotting/_tools/paga.py +++ b/scanpy/plotting/_tools/paga.py @@ -206,26 +206,19 @@ def _compute_pos( iterations = layout_kwds['iterations'] else: iterations = 500 - pos_list = forceatlas2.forceatlas2( - adjacency_solid, pos=init_coords, iterations=iterations - ) + pos_list = forceatlas2.forceatlas2(adjacency_solid, pos=init_coords, iterations=iterations) pos = {n: [p[0], -p[1]] for n, p in enumerate(pos_list)} elif layout == 'eq_tree': nx_g_tree = nx.Graph(adj_tree) pos = _utils.hierarchy_pos(nx_g_tree, root) if len(pos) < adjacency_solid.shape[0]: - raise ValueError( - 'This is a forest and not a single tree. ' - 'Try another `layout`, e.g., {\'fr\'}.' - ) + raise ValueError('This is a forest and not a single tree. ' 'Try another `layout`, e.g., {\'fr\'}.') else: # igraph layouts g = _sc_utils.get_igraph_from_adjacency(adjacency_solid) if 'rt' in layout: g_tree = _sc_utils.get_igraph_from_adjacency(adj_tree) - pos_list = g_tree.layout( - layout, root=root if isinstance(root, list) else [root] - ).coords + pos_list = g_tree.layout(layout, root=root if isinstance(root, list) else [root]).coords elif layout == 'circle': pos_list = g.layout(layout).coords else: @@ -240,9 +233,7 @@ def _compute_pos( init_pos[:, 1] *= -1 init_coords = init_pos.tolist() try: - pos_list = g.layout( - layout, seed=init_coords, weights='weight', **layout_kwds - ).coords + pos_list = g.layout(layout, seed=init_coords, weights='weight', **layout_kwds).coords except AttributeError: # hack for empty graphs... pos_list = g.layout(layout, seed=init_coords, **layout_kwds).coords pos = {n: [p[0], -p[1]] for n, p in enumerate(pos_list)} @@ -441,18 +432,12 @@ def paga( groups_key = adata.uns['paga']['groups'] def is_flat(x): - has_one_per_category = isinstance(x, cabc.Collection) and len(x) == len( - adata.obs[groups_key].cat.categories - ) + has_one_per_category = isinstance(x, cabc.Collection) and len(x) == len(adata.obs[groups_key].cat.categories) return has_one_per_category or x is None or isinstance(x, str) - if isinstance(colors, cabc.Mapping) and isinstance( - colors[next(iter(colors))], cabc.Mapping - ): + if isinstance(colors, cabc.Mapping) and isinstance(colors[next(iter(colors))], cabc.Mapping): # handle paga pie, remap string keys to integers - names_to_ixs = { - n: i for i, n in enumerate(adata.obs[groups_key].cat.categories) - } + names_to_ixs = {n: i for i, n in enumerate(adata.obs[groups_key].cat.categories)} colors = {names_to_ixs.get(n, n): v for n, v in colors.items()} if is_flat(colors): colors = [colors] @@ -473,21 +458,14 @@ def is_flat(x): if colorbar is None: var_names = adata.var_names if adata.raw is None else adata.raw.var_names colorbars = [ - ( - (c in adata.obs_keys() and adata.obs[c].dtype.name != 'category') - or (c in var_names) - ) - for c in colors + ((c in adata.obs_keys() and adata.obs[c].dtype.name != 'category') or (c in var_names)) for c in colors ] else: colorbars = [False for _ in colors] if isinstance(root, str): if root not in labels: - raise ValueError( - 'If `root` is a string, ' - f'it needs to be one of {labels} not {root!r}.' - ) + raise ValueError('If `root` is a string, ' f'it needs to be one of {labels} not {root!r}.') root = list(labels).index(root) if isinstance(root, cabc.Sequence) and root[0] in labels: root = [list(labels).index(r) for r in root] @@ -631,11 +609,7 @@ def _paga_graph( import networkx as nx node_labels = labels # rename for clarity - if ( - node_labels is not None - and isinstance(node_labels, str) - and node_labels != adata.uns['paga']['groups'] - ): + if node_labels is not None and isinstance(node_labels, str) and node_labels != adata.uns['paga']['groups']: raise ValueError( 'Provide a list of group labels for the PAGA groups {}, not {}.'.format( adata.uns['paga']['groups'], node_labels @@ -646,9 +620,9 @@ def _paga_graph( node_labels = adata.obs[groups_key].cat.categories if (colors is None or colors == groups_key) and groups_key is not None: - if groups_key + '_colors' not in adata.uns or len( - adata.obs[groups_key].cat.categories - ) != len(adata.uns[groups_key + '_colors']): + if groups_key + '_colors' not in adata.uns or len(adata.obs[groups_key].cat.categories) != len( + adata.uns[groups_key + '_colors'] + ): _utils.add_colors_for_categorical_sample_annotation(adata, groups_key) colors = adata.uns[groups_key + '_colors'] for iname, name in enumerate(adata.obs[groups_key].cat.categories): @@ -714,11 +688,7 @@ def _paga_graph( colors = x_color # plot continuous annotation - if ( - isinstance(colors, str) - and colors in adata.obs - and not is_categorical_dtype(adata.obs[colors]) - ): + if isinstance(colors, str) and colors in adata.obs and not is_categorical_dtype(adata.obs[colors]): x_color = [] cats = adata.obs[groups_key].cat.categories for icat, cat in enumerate(cats): @@ -727,11 +697,7 @@ def _paga_graph( colors = x_color # plot categorical annotation - if ( - isinstance(colors, str) - and colors in adata.obs - and is_categorical_dtype(adata.obs[colors]) - ): + if isinstance(colors, str) and colors in adata.obs and is_categorical_dtype(adata.obs[colors]): asso_names, asso_matrix = _sc_utils.compute_association_matrix_of_groups( adata, prediction=groups_key, @@ -739,16 +705,11 @@ def _paga_graph( normalization='reference' if normalize_to_color else 'prediction', ) _utils.add_colors_for_categorical_sample_annotation(adata, colors) - asso_colors = _sc_utils.get_associated_colors_of_groups( - adata.uns[colors + '_colors'], asso_matrix - ) + asso_colors = _sc_utils.get_associated_colors_of_groups(adata.uns[colors + '_colors'], asso_matrix) colors = asso_colors if len(colors) != len(node_labels): - raise ValueError( - f'Expected `colors` to be of length `{len(node_labels)}`, ' - f'found `{len(colors)}`.' - ) + raise ValueError(f'Expected `colors` to be of length `{len(node_labels)}`, ' f'found `{len(colors)}`.') # count number of connected components n_components, labels = scipy.sparse.csgraph.connected_components(adjacency_solid) @@ -764,13 +725,8 @@ def _paga_graph( adjacency_solid = adjacency_solid.tocsc()[:, labels == largest_component] colors = np.array(colors)[labels == largest_component] node_labels = np.array(node_labels)[labels == largest_component] - cats_dropped = ( - adata.obs[groups_key].cat.categories[labels != largest_component].tolist() - ) - logg.info( - 'Restricting graph to largest connected component by dropping categories\n' - f'{cats_dropped}' - ) + cats_dropped = adata.obs[groups_key].cat.categories[labels != largest_component].tolist() + logg.info('Restricting graph to largest connected component by dropping categories\n' f'{cats_dropped}') nx_g_solid = nx.Graph(adjacency_solid) if dashed_edges is not None: raise ValueError('`single_component` only if `dashed_edges` is `None`.') @@ -802,9 +758,7 @@ def _paga_graph( widths = np.clip(widths, min_edge_width, max_edge_width) with warnings.catch_warnings(): warnings.simplefilter("ignore") - nx.draw_networkx_edges( - nx_g_solid, pos, ax=ax, width=widths, edge_color='black' - ) + nx.draw_networkx_edges(nx_g_solid, pos, ax=ax, width=widths, edge_color='black') # draw directed edges else: adjacency_transitions = adata.uns['paga'][transitions].copy() @@ -817,9 +771,7 @@ def _paga_graph( widths = base_edge_width * np.array(widths) if min_edge_width is not None or max_edge_width is not None: widths = np.clip(widths, min_edge_width, max_edge_width) - nx.draw_networkx_edges( - g_dir, pos, ax=ax, width=widths, edge_color='black', arrowsize=arrowsize - ) + nx.draw_networkx_edges(g_dir, pos, ax=ax, width=widths, edge_color='black', arrowsize=arrowsize) if export_to_gexf: if isinstance(colors[0], tuple): @@ -851,21 +803,15 @@ def _paga_graph( else: groups_sizes = np.ones(len(node_labels)) base_scale_scatter = 2000 - base_pie_size = ( - base_scale_scatter / (np.sqrt(adjacency_solid.shape[0]) + 10) * node_size_scale - ) + base_pie_size = base_scale_scatter / (np.sqrt(adjacency_solid.shape[0]) + 10) * node_size_scale median_group_size = np.median(groups_sizes) - groups_sizes = base_pie_size * np.power( - groups_sizes / median_group_size, node_size_power - ) + groups_sizes = base_pie_size * np.power(groups_sizes / median_group_size, node_size_power) if fontsize is None: fontsize = rcParams['legend.fontsize'] if fontoutline is not None: text_kwds = dict(text_kwds) - text_kwds['path_effects'] = [ - patheffects.withStroke(linewidth=fontoutline, foreground='w') - ] + text_kwds['path_effects'] = [patheffects.withStroke(linewidth=fontoutline, foreground='w')] # usual scatter plot if not isinstance(colors[0], cabc.Mapping): n_groups = len(pos_array) @@ -893,8 +839,7 @@ def _paga_graph( for ix, (xx, yy) in enumerate(zip(pos_array[:, 0], pos_array[:, 1])): if not isinstance(colors[ix], cabc.Mapping): raise ValueError( - f'{colors[ix]} is neither a dict of valid ' - 'matplotlib colors nor a valid matplotlib color.' + f'{colors[ix]} is neither a dict of valid ' 'matplotlib colors nor a valid matplotlib color.' ) color_single = colors[ix].keys() fracs = [colors[ix][c] for c in color_single] @@ -905,10 +850,7 @@ def _paga_graph( color_single.append('grey') fracs.append(1 - sum(fracs)) elif not np.isclose(total, 1): - raise ValueError( - f'Expected fractions for node `{ix}` to be ' - f'close to 1, found `{total}`.' - ) + raise ValueError(f'Expected fractions for node `{ix}` to be ' f'close to 1, found `{total}`.') cumsum = np.cumsum(fracs) cumsum = cumsum / cumsum[-1] @@ -922,9 +864,7 @@ def _paga_graph( xy = np.column_stack([x, y]) s = np.abs(xy).max() - sct = ax.scatter( - [xx], [yy], marker=xy, s=s ** 2 * groups_sizes[ix], color=color - ) + sct = ax.scatter([xx], [yy], marker=xy, s=s ** 2 * groups_sizes[ix], color=color) if node_labels is not None: ax.text( @@ -948,9 +888,7 @@ def paga_path( use_raw: bool = True, annotations: Sequence[str] = ('dpt_pseudotime',), color_map: Union[str, Colormap, None] = None, - color_maps_annotations: Mapping[str, Union[str, Colormap]] = MappingProxyType( - dict(dpt_pseudotime='Greys') - ), + color_maps_annotations: Mapping[str, Union[str, Colormap]] = MappingProxyType(dict(dpt_pseudotime='Greys')), palette_groups: Optional[Sequence[str]] = None, n_avg: int = 1, groups_key: Optional[str] = None, @@ -1033,17 +971,13 @@ def paga_path( if groups_key is None: if 'groups' not in adata.uns['paga']: - raise KeyError( - 'Pass the key of the grouping with which you ran PAGA, ' - 'using the parameter `groups_key`.' - ) + raise KeyError('Pass the key of the grouping with which you ran PAGA, ' 'using the parameter `groups_key`.') groups_key = adata.uns['paga']['groups'] groups_names = adata.obs[groups_key].cat.categories if 'dpt_pseudotime' not in adata.obs.keys(): raise ValueError( - '`pl.paga_path` requires computation of a pseudotime `tl.dpt` ' - 'for ordering at single-cell resolution' + '`pl.paga_path` requires computation of a pseudotime `tl.dpt` ' 'for ordering at single-cell resolution' ) if palette_groups is None: @@ -1082,9 +1016,7 @@ def moving_average(a): for ikey, key in enumerate(keys): x = [] for igroup, group in enumerate(nodes_ints): - idcs = np.arange(adata.n_obs)[ - adata.obs[groups_key].values == nodes_strs[igroup] - ] + idcs = np.arange(adata.n_obs)[adata.obs[groups_key].values == nodes_strs[igroup]] if len(idcs) == 0: raise ValueError( 'Did not find data points that match ' @@ -1093,9 +1025,7 @@ def moving_average(a): 'actually contains what you expect.' ) idcs_group = np.argsort( - adata.obs['dpt_pseudotime'].values[ - adata.obs[groups_key].values == nodes_strs[igroup] - ] + adata.obs['dpt_pseudotime'].values[adata.obs[groups_key].values == nodes_strs[igroup]] ) idcs = idcs[idcs_group] values = ( @@ -1221,9 +1151,7 @@ def moving_average(a): ) arr = np.array(anno_dict[anno])[None, :] if anno not in color_maps_annotations: - color_map_anno = ( - 'Vega10' if is_categorical_dtype(adata.obs[anno]) else 'Greys' - ) + color_map_anno = 'Vega10' if is_categorical_dtype(adata.obs[anno]) else 'Greys' else: color_map_anno = color_maps_annotations[anno] img = anno_axis.imshow( diff --git a/scanpy/plotting/_tools/scatterplots.py b/scanpy/plotting/_tools/scatterplots.py index 154ffb7a91..c60946698e 100644 --- a/scanpy/plotting/_tools/scatterplots.py +++ b/scanpy/plotting/_tools/scatterplots.py @@ -145,8 +145,7 @@ def embedding( use_raw = layer is None and adata.raw is not None if use_raw and layer is not None: raise ValueError( - "Cannot use both a layer and the raw representation. Was passed:" - f"use_raw={use_raw}, layer={layer}." + "Cannot use both a layer and the raw representation. Was passed:" f"use_raw={use_raw}, layer={layer}." ) if wspace is None: @@ -154,10 +153,7 @@ def embedding( # current figure size wspace = 0.75 / rcParams['figure.figsize'][0] + 0.02 if adata.raw is None and use_raw: - raise ValueError( - "`use_raw` is set to True but AnnData object does not have raw. " - "Please check." - ) + raise ValueError("`use_raw` is set to True but AnnData object does not have raw. " "Please check.") # turn color into a python list color = [color] if isinstance(color, str) or color is None else list(color) if title is not None: @@ -166,24 +162,17 @@ def embedding( # get the points position and the components list # (only if components is not None) - data_points, components_list = _get_data_points( - adata, basis, projection, components, scale_factor - ) + data_points, components_list = _get_data_points(adata, basis, projection, components, scale_factor) # Setup layout. # Most of the code is for the case when multiple plots are required # 'color' is a list of names that want to be plotted. # Eg. ['Gene1', 'louvain', 'Gene2']. # component_list is a list of components [[0,1], [1,2]] - if ( - not isinstance(color, str) - and isinstance(color, cabc.Sequence) - and len(color) > 1 - ) or len(components_list) > 1: + if (not isinstance(color, str) and isinstance(color, cabc.Sequence) and len(color) > 1) or len(components_list) > 1: if ax is not None: raise ValueError( - "Cannot specify `ax` when plotting multiple panels " - "(each for a given value of 'color')." + "Cannot specify `ax` when plotting multiple panels " "(each for a given value of 'color')." ) if len(components_list) == 0: components_list = [None] @@ -238,9 +227,7 @@ def embedding( # color=gene1, components=[1,2], color=gene1, components=[2,3], # color=gene2, components = [1, 2], color=gene2, components=[2,3], # ] - for count, (value_to_plot, component_idx) in enumerate( - itertools.product(color, idx_components) - ): + for count, (value_to_plot, component_idx) in enumerate(itertools.product(color, idx_components)): color_source_vector = _get_color_source_vector( adata, value_to_plot, @@ -294,6 +281,7 @@ def embedding( ) ax.set_title(value_to_plot) +<<<<<<< HEAD if not categorical: vmin_float, vmax_float, vcenter_float, norm_obj = _get_vboundnorm( vmin, vmax, vcenter, norm, count, color_vector @@ -306,6 +294,13 @@ def embedding( ) else: normalize = None +======= + # check vmin and vmax options + if categorical: + kwargs['vmin'] = kwargs['vmax'] = None + else: + kwargs['vmin'], kwargs['vmax'] = _get_vmin_vmax(vmin, vmax, count, color_vector) +>>>>>>> 40dc2c3b (add flake8 pre-commit) # make the scatter plot if projection == '3d': @@ -418,9 +413,7 @@ def embedding( continue if legend_fontoutline is not None: - path_effect = [ - patheffects.withStroke(linewidth=legend_fontoutline, foreground='w') - ] + path_effect = [patheffects.withStroke(linewidth=legend_fontoutline, foreground='w')] else: path_effect = None @@ -578,17 +571,11 @@ def _wraps_plot_scatter(wrapper): wrapper_params.pop("adata") params.update(wrapper_params) - annotations = { - k: v.annotation - for k, v in params.items() - if v.annotation != inspect.Parameter.empty - } + annotations = {k: v.annotation for k, v in params.items() if v.annotation != inspect.Parameter.empty} if wrapper_sig.return_annotation is not inspect.Signature.empty: annotations["return"] = wrapper_sig.return_annotation - wrapper.__signature__ = inspect.Signature( - list(params.values()), return_annotation=wrapper_sig.return_annotation - ) + wrapper.__signature__ = inspect.Signature(list(params.values()), return_annotation=wrapper_sig.return_annotation) wrapper.__annotations__ = annotations return wrapper @@ -677,9 +664,7 @@ def diffmap(adata, **kwargs) -> Union[Axes, List[Axes], None]: scatter_bulk=doc_scatter_embedding, show_save_ax=doc_show_save_ax, ) -def draw_graph( - adata: AnnData, *, layout: Optional[_IGraphLayout] = None, **kwargs -) -> Union[Axes, List[Axes], None]: +def draw_graph(adata: AnnData, *, layout: Optional[_IGraphLayout] = None, **kwargs) -> Union[Axes, List[Axes], None]: """\ Scatter plot in graph-drawing basis. @@ -702,9 +687,7 @@ def draw_graph( basis = 'draw_graph_' + layout if 'X_' + basis not in adata.obsm_keys(): raise ValueError( - 'Did not find {} in adata.obs. Did you compute layout {}?'.format( - 'draw_graph_' + layout, layout - ) + 'Did not find {} in adata.obs. Did you compute layout {}?'.format('draw_graph_' + layout, layout) ) return embedding(adata, basis, **kwargs) @@ -742,15 +725,12 @@ def pca( If `show==False` a :class:`~matplotlib.axes.Axes` or a list of it. """ if not annotate_var_explained: - return embedding( - adata, 'pca', show=show, return_fig=return_fig, save=save, **kwargs - ) + return embedding(adata, 'pca', show=show, return_fig=return_fig, save=save, **kwargs) else: if 'pca' not in adata.obsm.keys() and 'X_pca' not in adata.obsm.keys(): raise KeyError( - f"Could not find entry in `obsm` for 'pca'.\n" - f"Available keys are: {list(adata.obsm.keys())}." + f"Could not find entry in `obsm` for 'pca'.\n" f"Available keys are: {list(adata.obsm.keys())}." ) label_dict = { @@ -862,9 +842,7 @@ def spatial( library_id, spatial_data = _check_spatial_data(adata.uns, library_id) img, img_key = _check_img(spatial_data, img, img_key, bw=bw) spot_size = _check_spot_size(spatial_data, spot_size) - scale_factor = _check_scale_factor( - spatial_data, img_key=img_key, scale_factor=scale_factor - ) + scale_factor = _check_scale_factor(spatial_data, img_key=img_key, scale_factor=scale_factor) crop_coord = _check_crop_coord(crop_coord, scale_factor) na_color = _check_na_color(na_color, img=img) @@ -931,8 +909,7 @@ def _get_data_points( basis_key = f"X_{basis}" else: raise KeyError( - f"Could not find entry in `obsm` for '{basis}'.\n" - f"Available keys are: {list(adata.obsm.keys())}." + f"Could not find entry in `obsm` for '{basis}'.\n" f"Available keys are: {list(adata.obsm.keys())}." ) n_dims = 2 @@ -952,9 +929,7 @@ def _get_data_points( r_value = 3 if projection == '3d' else 2 _components_list = np.arange(adata.obsm[basis_key].shape[1]) + 1 - components = [ - ",".join(map(str, x)) for x in combinations(_components_list, r=r_value) - ] + components = [",".join(map(str, x)) for x in combinations(_components_list, r=r_value)] components_list = [] offset = 0 @@ -966,9 +941,7 @@ def _get_data_points( if isinstance(components, str): # eg: components='1,2' - components_list.append( - tuple(int(x.strip()) - 1 + offset for x in components.split(',')) - ) + components_list.append(tuple(int(x.strip()) - 1 + offset for x in components.split(','))) elif isinstance(components, cabc.Sequence): if isinstance(components[0], int): @@ -980,14 +953,11 @@ def _get_data_points( # More than one component can be given and is stored # as a new item of components_list for comp in components: - components_list.append( - tuple(int(x.strip()) - 1 + offset for x in comp.split(',')) - ) + components_list.append(tuple(int(x.strip()) - 1 + offset for x in comp.split(','))) else: raise ValueError( - "Given components: '{}' are not valid. Please check. " - "A valid example is `components='2,3'`" + "Given components: '{}' are not valid. Please check. " "A valid example is `components='2,3'`" ) # check if the components are present in the data try: @@ -996,16 +966,13 @@ def _get_data_points( data_points.append(adata.obsm[basis_key][:, comp]) except Exception: raise ValueError( - "Given components: '{}' are not valid. Please check. " - "A valid example is `components='2,3'`" + "Given components: '{}' are not valid. Please check. " "A valid example is `components='2,3'`" ) if basis == 'diffmap': # remove the offset added in the case of diffmap, such that # plot_scatter can print the labels correctly. - components_list = [ - tuple(number - 1 for number in comp) for comp in components_list - ] + components_list = [tuple(number - 1 for number in comp) for comp in components_list] else: data_points = [np.array(adata.obsm[basis_key])[:, offset : offset + n_dims]] components_list = [] @@ -1032,9 +999,7 @@ def _add_categorical_legend( """Add a legend to the passed Axes.""" if na_in_legend and pd.isnull(color_source_vector).any(): if "NA" in color_source_vector: - raise NotImplementedError( - "No fallback for null labels has been defined if NA already in categories." - ) + raise NotImplementedError("No fallback for null labels has been defined if NA already in categories.") color_source_vector = color_source_vector.add_categories("NA").fillna("NA") palette = palette.copy() palette["NA"] = na_color @@ -1058,11 +1023,7 @@ def _add_categorical_legend( ) elif legend_loc == 'on data': # identify centroids to put labels - all_pos = ( - pd.DataFrame(scatter_array, columns=["x", "y"]) - .groupby(color_source_vector, observed=True) - .median() - ) + all_pos = pd.DataFrame(scatter_array, columns=["x", "y"]).groupby(color_source_vector, observed=True).median() for label, x_pos, y_pos in all_pos.itertuples(): ax.text( @@ -1080,9 +1041,7 @@ def _add_categorical_legend( _utils._tmp_cluster_pos = all_pos.values -def _get_color_source_vector( - adata, value_to_plot, use_raw=False, gene_symbols=None, layer=None, groups=None -): +def _get_color_source_vector(adata, value_to_plot, use_raw=False, gene_symbols=None, layer=None, groups=None): """ Get array from adata that colors will be based on. """ @@ -1092,11 +1051,7 @@ def _get_color_source_vector( # _color_vector handles this. # https://github.com/matplotlib/matplotlib/issues/18294 return np.broadcast_to(np.nan, adata.n_obs) - if ( - gene_symbols is not None - and value_to_plot not in adata.obs.columns - and value_to_plot not in adata.var_names - ): + if gene_symbols is not None and value_to_plot not in adata.obs.columns and value_to_plot not in adata.var_names: # We should probably just make an index for this, and share it over runs value_to_plot = adata.var.index[adata.var[gene_symbols] == value_to_plot][ 0 @@ -1115,9 +1070,7 @@ def _get_palette(adata, values_key: str, palette=None): values = pd.Categorical(adata.obs[values_key]) if palette: _utils._set_colors_for_categorical_obs(adata, values_key, palette) - elif color_key not in adata.uns or len(adata.uns[color_key]) < len( - values.categories - ): + elif color_key not in adata.uns or len(adata.uns[color_key]) < len(values.categories): # set a default palette in case that no colors or few colors are found _utils._set_default_colors_for_categorical_obs(adata, values_key) else: @@ -1125,9 +1078,7 @@ def _get_palette(adata, values_key: str, palette=None): return dict(zip(values.categories, adata.uns[color_key])) -def _color_vector( - adata, values_key: str, values, palette, na_color="lightgray" -) -> Tuple[np.ndarray, bool]: +def _color_vector(adata, values_key: str, values, palette, na_color="lightgray") -> Tuple[np.ndarray, bool]: """ Map array of values to array of hex (plus alpha) codes. @@ -1146,10 +1097,7 @@ def _color_vector( if not is_categorical_dtype(values): return values, False else: # is_categorical_dtype(values) - color_map = { - k: to_hex(v) - for k, v in _get_palette(adata, values_key, palette=palette).items() - } + color_map = {k: to_hex(v) for k, v in _get_palette(adata, values_key, palette=palette).items()} # If color_map does not have unique values, this can be slow as the # result is not categorical color_vector = values.map(color_map) @@ -1182,19 +1130,14 @@ def _basis2name(basis): return component_name -def _check_spot_size( - spatial_data: Optional[Mapping], spot_size: Optional[float] -) -> float: +def _check_spot_size(spatial_data: Optional[Mapping], spot_size: Optional[float]) -> float: """ Resolve spot_size value. This is a required argument for spatial plots. """ if spatial_data is None and spot_size is None: - raise ValueError( - "When .uns['spatial'][library_id] does not exist, spot_size must be " - "provided directly." - ) + raise ValueError("When .uns['spatial'][library_id] does not exist, spot_size must be " "provided directly.") elif spot_size is None: return spatial_data['scalefactors']['spot_diameter_fullres'] else: @@ -1215,9 +1158,7 @@ def _check_scale_factor( return 1.0 -def _check_spatial_data( - uns: Mapping, library_id: Union[Empty, None, str] -) -> Tuple[Optional[str], Optional[Mapping]]: +def _check_spatial_data(uns: Mapping, library_id: Union[Empty, None, str]) -> Tuple[Optional[str], Optional[Mapping]]: """ Given a mapping, try and extract a library id/ mapping with spatial data. @@ -1274,9 +1215,7 @@ def _check_crop_coord( return crop_coord -def _check_na_color( - na_color: Optional[ColorLike], *, img: Optional[np.ndarray] = None -) -> ColorLike: +def _check_na_color(na_color: Optional[ColorLike], *, img: Optional[np.ndarray] = None) -> ColorLike: if na_color is None: if img is not None: na_color = (0.0, 0.0, 0.0, 0.0) diff --git a/scanpy/plotting/_utils.py b/scanpy/plotting/_utils.py index 9d7af70f5a..e4bf1209fd 100644 --- a/scanpy/plotting/_utils.py +++ b/scanpy/plotting/_utils.py @@ -70,9 +70,7 @@ def matrix( ax.set_xticks(range(len(xticks)), xticks, rotation='vertical') if yticks is not None: ax.set_yticks(range(len(yticks)), yticks) - pl.colorbar( - img, shrink=colorbar_shrink, ax=ax - ) # need a figure instance for colorbar + pl.colorbar(img, shrink=colorbar_shrink, ax=ax) # need a figure instance for colorbar savefig_or_show('matrix', show=show, save=save) @@ -157,9 +155,7 @@ def timeseries_subplot( ax.legend(frameon=False) -def timeseries_as_heatmap( - X: np.ndarray, var_names: Collection[str] = (), highlights_x=(), color_map=None -): +def timeseries_as_heatmap(X: np.ndarray, var_names: Collection[str] = (), highlights_x=(), color_map=None): """\ Plot timeseries as heatmap. @@ -268,10 +264,7 @@ def savefig(writekey, dpi=None, ext=None): """ if dpi is None: # we need this as in notebooks, the internal figures are also influenced by 'savefig.dpi' this... - if ( - not isinstance(rcParams['savefig.dpi'], str) - and rcParams['savefig.dpi'] < 150 - ): + if not isinstance(rcParams['savefig.dpi'], str) and rcParams['savefig.dpi'] < 150: if settings._low_resolution_warning: logg.warning( 'You are using a low resolution (dpi<150) for saving figures.\n' @@ -359,9 +352,7 @@ def _validate_palette(adata, key): adata.uns[color_key] = _palette -def _set_colors_for_categorical_obs( - adata, value_to_plot, palette: Union[str, Sequence[str], Cycler] -): +def _set_colors_for_categorical_obs(adata, value_to_plot, palette: Union[str, Sequence[str], Cycler]): """ Sets the adata.uns[value_to_plot + '_colors'] according to the given palette @@ -410,10 +401,7 @@ def _set_colors_for_categorical_obs( if color in additional_colors: color = additional_colors[color] else: - raise ValueError( - "The following color value of the given palette " - f"is not valid: {color}" - ) + raise ValueError("The following color value of the given palette " f"is not valid: {color}") _color_list.append(color) palette = cycler(color=_color_list) @@ -473,9 +461,7 @@ def _set_default_colors_for_categorical_obs(adata, value_to_plot): adata.uns[value_to_plot + '_colors'] = palette[:length] -def add_colors_for_categorical_sample_annotation( - adata, key, palette=None, force_update_colors=False -): +def add_colors_for_categorical_sample_annotation(adata, key, palette=None, force_update_colors=False): color_key = f"{key}_colors" colors_needed = len(adata.obs[key].cat.categories) @@ -518,14 +504,10 @@ def plot_edges(axs, adata, basis, edges_width, edges_color, neighbors_key=None): def plot_arrows(axs, adata, basis, arrows_kwds=None): if not isinstance(axs, cabc.Sequence): axs = [axs] - v_prefix = next( - (p for p in ['velocity', 'Delta'] if f'{p}_{basis}' in adata.obsm), None - ) + v_prefix = next((p for p in ['velocity', 'Delta'] if f'{p}_{basis}' in adata.obsm), None) if v_prefix is None: raise ValueError( - "`arrows=True` requires " - f"`'velocity_{basis}'` from scvelo or " - f"`'Delta_{basis}'` from velocyto." + "`arrows=True` requires " f"`'velocity_{basis}'` from scvelo or " f"`'Delta_{basis}'` from velocyto." ) if v_prefix == 'velocity': logg.warning( @@ -607,9 +589,7 @@ def setup_axes( if show_ticks: base_width *= 1.1 - draw_region_width = ( - base_width - left_offset - top_offset - 0.5 - ) # this is kept constant throughout + draw_region_width = base_width - left_offset - top_offset - 0.5 # this is kept constant throughout right_margin_factor = sum([1 + right_margin for right_margin in right_margin_list]) width_without_offsets = ( @@ -630,9 +610,7 @@ def setup_axes( left_positions = [left_offset_frac, left_offset_frac + draw_region_width_frac] for i in range(1, len(panels)): right_margin = right_margin_list[i - 1] - left_positions.append( - left_positions[-1] + right_margin * draw_region_width_frac - ) + left_positions.append(left_positions[-1] + right_margin * draw_region_width_frac) left_positions.append(left_positions[-1] + draw_region_width_frac) panel_pos = [[bottom_offset], [1 - top_offset], left_positions] @@ -736,10 +714,7 @@ def scatter_base( ) if colorbars[icolor]: width = 0.006 * draw_region_width / len(colors) - left = ( - panel_pos[2][2 * icolor + 1] - + (1.2 if projection == '3d' else 0.2) * width - ) + left = panel_pos[2][2 * icolor + 1] + (1.2 if projection == '3d' else 0.2) * width rectangle = [left, bottom, width, height] fig = pl.gcf() ax_cb = fig.add_axes(rectangle) @@ -762,11 +737,7 @@ def scatter_base( s=10, zorder=20, ) - highlight_text = ( - highlights_labels[iihighlight] - if len(highlights_labels) > 0 - else str(ihighlight) - ) + highlight_text = highlights_labels[iihighlight] if len(highlights_labels) > 0 else str(ihighlight) # the following is a Python 2 compatibility hack ax.text( *([d[0] for d in data] + [highlight_text]), @@ -781,10 +752,7 @@ def scatter_base( ax.set_zticks([]) # set default axis_labels if axis_labels is None: - axis_labels = [ - [component_name + str(i) for i in component_indexnames] - for _ in range(len(axs)) - ] + axis_labels = [[component_name + str(i) for i in component_indexnames] for _ in range(len(axs))] else: axis_labels = [axis_labels for _ in range(len(axs))] for iax, ax in enumerate(axs): @@ -938,7 +906,11 @@ def make_pos(pos, node=root, currentLevel=0, parent=None, vert_loc=0): if levels is None: levels = make_levels({}) else: +<<<<<<< HEAD levels = {l: {TOTAL: levels[l], CURRENT: 0} for l in levels} +======= + levels = {level: {TOTAL: levels[level], CURRENT: 0} for level in levels} +>>>>>>> 7a096bf9 (add flake8 pre-commit) vert_gap = height / (max([level for level in levels]) + 1) return make_pos({}) @@ -972,9 +944,7 @@ def zoom(ax, xy='x', factor=1): ---------- """ limits = ax.get_xlim() if xy == 'x' else ax.get_ylim() - new_limits = 0.5 * (limits[0] + limits[1]) + 1.0 / factor * np.array( - (-0.5, 0.5) - ) * (limits[1] - limits[0]) + new_limits = 0.5 * (limits[0] + limits[1]) + 1.0 / factor * np.array((-0.5, 0.5)) * (limits[1] - limits[0]) if xy == 'x': ax.set_xlim(new_limits) else: @@ -1056,9 +1026,7 @@ def check_projection(projection): mpl_version = parse(mpl.__version__) if mpl_version < parse("3.3.3"): - raise ImportError( - f"3d plotting requires matplotlib > 3.3.3. Found {mpl.__version__}" - ) + raise ImportError(f"3d plotting requires matplotlib > 3.3.3. Found {mpl.__version__}") def circles(x, y, s, ax, marker=None, c='b', vmin=None, vmax=None, **kwargs): diff --git a/scanpy/plotting/palettes.py b/scanpy/plotting/palettes.py index 3ea963de99..f173719049 100644 --- a/scanpy/plotting/palettes.py +++ b/scanpy/plotting/palettes.py @@ -210,6 +210,4 @@ def _plot_color_cycle(clists: Mapping[str, Sequence[str]]): if __name__ == '__main__': - _plot_color_cycle( - {name: colors for name, colors in globals().items() if isinstance(colors, list)} - ) + _plot_color_cycle({name: colors for name, colors in globals().items() if isinstance(colors, list)}) diff --git a/scanpy/preprocessing/_combat.py b/scanpy/preprocessing/_combat.py index e2d8140bca..ee5761798c 100644 --- a/scanpy/preprocessing/_combat.py +++ b/scanpy/preprocessing/_combat.py @@ -10,9 +10,7 @@ from .._utils import sanitize_anndata -def _design_matrix( - model: pd.DataFrame, batch_key: str, batch_levels: Collection[str] -) -> pd.DataFrame: +def _design_matrix(model: pd.DataFrame, batch_key: str, batch_levels: Collection[str]) -> pd.DataFrame: """\ Computes a simple design matrix. @@ -44,9 +42,7 @@ def _design_matrix( if other_cols: col_repr = " + ".join("Q('{}')".format(x) for x in other_cols) - factor_matrix = patsy.dmatrix( - "~ 0 + {}".format(col_repr), model[other_cols], return_type="dataframe" - ) + factor_matrix = patsy.dmatrix("~ 0 + {}".format(col_repr), model[other_cols], return_type="dataframe") design = pd.concat((design, factor_matrix), axis=1) logg.info(f"Found {len(other_cols)} categorical variables:") @@ -109,9 +105,7 @@ def _standardize_data( # Compute the means if np.sum(var_pooled == 0) > 0: print(f'Found {np.sum(var_pooled == 0)} genes with zero variance.') - stand_mean = np.dot( - grand_mean.T.reshape((len(grand_mean), 1)), np.ones((1, int(n_array))) - ) + stand_mean = np.dot(grand_mean.T.reshape((len(grand_mean), 1)), np.ones((1, int(n_array)))) tmp = np.array(design.copy()) tmp[:, :n_batch] = 0 stand_mean += np.dot(tmp, B_hat).T @@ -175,9 +169,7 @@ def combat( cov_exist = np.isin(covariates, adata.obs_keys()) if np.any(~cov_exist): missing_cov = np.array(covariates)[~cov_exist].tolist() - raise ValueError( - 'Could not find the covariate(s) {!r} in adata.obs'.format(missing_cov) - ) + raise ValueError('Could not find the covariate(s) {!r} in adata.obs'.format(missing_cov)) if key in covariates: raise ValueError('Batch key and covariates cannot overlap') @@ -209,9 +201,7 @@ def combat( logg.info("Fitting L/S model and finding priors\n") batch_design = design[design.columns[:n_batch]] # first estimate of the additive batch effect - gamma_hat = ( - la.inv(batch_design.T @ batch_design) @ batch_design.T @ s_data.T - ).values + gamma_hat = (la.inv(batch_design.T @ batch_design) @ batch_design.T @ s_data.T).values delta_hat = [] # first estimate for the multiplicative batch effect @@ -260,10 +250,7 @@ def combat( dsq = np.sqrt(delta_star[j, :]) dsq = dsq.reshape((len(dsq), 1)) denom = np.dot(dsq, np.ones((1, n_batches[j]))) - numer = np.array( - bayesdata.iloc[:, batch_idxs] - - np.dot(batch_design.iloc[batch_idxs], gamma_star).T - ) + numer = np.array(bayesdata.iloc[:, batch_idxs] - np.dot(batch_design.iloc[batch_idxs], gamma_star).T) bayesdata.iloc[:, batch_idxs] = numer / denom vpsq = np.sqrt(var_pooled).reshape((len(var_pooled), 1)) @@ -329,16 +316,12 @@ def _it_sol( # in the loop, gamma and delta are updated together. they depend on each other. we iterate until convergence. while change > conv: g_new = (t2 * n * g_hat + d_old * g_bar) / (t2 * n + d_old) - sum2 = s_data - g_new.reshape((g_new.shape[0], 1)) @ np.ones( - (1, s_data.shape[1]) - ) + sum2 = s_data - g_new.reshape((g_new.shape[0], 1)) @ np.ones((1, s_data.shape[1])) sum2 = sum2 ** 2 sum2 = sum2.sum(axis=1) d_new = (0.5 * sum2 + b) / (n / 2.0 + a - 1.0) - change = max( - (abs(g_new - g_old) / g_old).max(), (abs(d_new - d_old) / d_old).max() - ) + change = max((abs(g_new - g_old) / g_old).max(), (abs(d_new - d_old) / d_old).max()) g_old = g_new # .copy() d_old = d_new # .copy() count = count + 1 diff --git a/scanpy/preprocessing/_deprecated/__init__.py b/scanpy/preprocessing/_deprecated/__init__.py index 2bb8730540..0e768454c1 100644 --- a/scanpy/preprocessing/_deprecated/__init__.py +++ b/scanpy/preprocessing/_deprecated/__init__.py @@ -36,15 +36,9 @@ def normalize_per_cell_weinreb16_deprecated( gene_subset = np.all(X <= counts_per_cell[:, None] * max_fraction, axis=0) if issparse(X): gene_subset = gene_subset.A1 - tc_include = ( - X[:, gene_subset].sum(1).A1 if issparse(X) else X[:, gene_subset].sum(1) - ) + tc_include = X[:, gene_subset].sum(1).A1 if issparse(X) else X[:, gene_subset].sum(1) - X_norm = ( - X.multiply(csr_matrix(1 / tc_include[:, None])) - if issparse(X) - else X / tc_include[:, None] - ) + X_norm = X.multiply(csr_matrix(1 / tc_include[:, None])) if issparse(X) else X / tc_include[:, None] if mult_with_mean: X_norm *= np.mean(counts_per_cell) diff --git a/scanpy/preprocessing/_deprecated/highly_variable_genes.py b/scanpy/preprocessing/_deprecated/highly_variable_genes.py index 570a5f9f25..86d68c3a77 100644 --- a/scanpy/preprocessing/_deprecated/highly_variable_genes.py +++ b/scanpy/preprocessing/_deprecated/highly_variable_genes.py @@ -104,9 +104,7 @@ def filter_genes_dispersion( If a data matrix `X` is passed, the annotation is returned as `np.recarray` with the same information stored in fields: `gene_subset`, `means`, `dispersions`, `dispersion_norm`. """ - if n_top_genes is not None and not all( - x is None for x in [min_disp, max_disp, min_mean, max_mean] - ): + if n_top_genes is not None and not all(x is None for x in [min_disp, max_disp, min_mean, max_mean]): logg.info('If you pass `n_top_genes`, all cutoffs are ignored.') if min_disp is None: min_disp = 0.5 @@ -170,8 +168,7 @@ def filter_genes_dispersion( disp_mean_bin[one_gene_per_bin] = 0 # actually do the normalization df['dispersion_norm'] = ( - df['dispersion'].values # use values here as index differs - - disp_mean_bin[df['mean_bin'].values].values + df['dispersion'].values - disp_mean_bin[df['mean_bin'].values].values # use values here as index differs ) / disp_std_bin[df['mean_bin'].values].values elif flavor == 'cell_ranger': from statsmodels import robust @@ -187,9 +184,7 @@ def filter_genes_dispersion( warnings.simplefilter('ignore') disp_mad_bin = disp_grouped.apply(robust.mad) df['dispersion_norm'] = ( - np.abs( - df['dispersion'].values - disp_median_bin[df['mean_bin'].values].values - ) + np.abs(df['dispersion'].values - disp_median_bin[df['mean_bin'].values].values) / disp_mad_bin[df['mean_bin'].values].values ) else: @@ -197,15 +192,10 @@ def filter_genes_dispersion( dispersion_norm = df['dispersion_norm'].values.astype('float32') if n_top_genes is not None: dispersion_norm = dispersion_norm[~np.isnan(dispersion_norm)] - dispersion_norm[ - ::-1 - ].sort() # interestingly, np.argpartition is slightly slower + dispersion_norm[::-1].sort() # interestingly, np.argpartition is slightly slower disp_cut_off = dispersion_norm[n_top_genes - 1] gene_subset = df['dispersion_norm'].values >= disp_cut_off - logg.debug( - f'the {n_top_genes} top genes correspond to a ' - f'normalized dispersion cutoff of {disp_cut_off}' - ) + logg.debug(f'the {n_top_genes} top genes correspond to a ' f'normalized dispersion cutoff of {disp_cut_off}') else: max_disp = np.inf if max_disp is None else max_disp dispersion_norm[np.isnan(dispersion_norm)] = 0 # similar to Seurat diff --git a/scanpy/preprocessing/_highly_variable_genes.py b/scanpy/preprocessing/_highly_variable_genes.py index 7d0ae3f235..591a0d97cd 100644 --- a/scanpy/preprocessing/_highly_variable_genes.py +++ b/scanpy/preprocessing/_highly_variable_genes.py @@ -52,16 +52,19 @@ def _highly_variable_genes_seurat_v3( try: from skmisc.loess import loess except ImportError: - raise ImportError( - 'Please install skmisc package via `pip install --user scikit-misc' - ) + raise ImportError('Please install skmisc package via `pip install --user scikit-misc') X = adata.layers[layer] if layer is not None else adata.X +<<<<<<< HEAD if check_values and (check_nonnegative_integers(X) == False): warnings.warn( "`flavor='seurat_v3'` expects raw count data, but non-integers were found.", UserWarning, ) +======= + if check_nonnegative_integers(X) is False: + raise ValueError("`pp.highly_variable_genes` with `flavor='seurat_v3'` expects " "raw count data.") +>>>>>>> 40dc2c3b (add flake8 pre-commit) if batch_key is None: batch_info = pd.Categorical(np.zeros(adata.shape[0], dtype=int)) @@ -110,9 +113,7 @@ def _highly_variable_genes_seurat_v3( batch_counts_sum = batch_counts.sum(axis=0) norm_gene_var = (1 / ((N - 1) * np.square(reg_std))) * ( - (N * np.square(mean)) - + squared_batch_counts_sum - - 2 * batch_counts_sum * mean + (N * np.square(mean)) + squared_batch_counts_sum - 2 * batch_counts_sum * mean ) norm_gene_vars.append(norm_gene_var.reshape(1, -1)) @@ -122,9 +123,7 @@ def _highly_variable_genes_seurat_v3( # this is done in SelectIntegrationFeatures() in Seurat v3 ranked_norm_gene_vars = ranked_norm_gene_vars.astype(np.float32) - num_batches_high_var = np.sum( - (ranked_norm_gene_vars < n_top_genes).astype(int), axis=0 - ) + num_batches_high_var = np.sum((ranked_norm_gene_vars < n_top_genes).astype(int), axis=0) ranked_norm_gene_vars[ranked_norm_gene_vars >= n_top_genes] = np.nan ma_ranked = np.ma.masked_invalid(ranked_norm_gene_vars) median_ranked = np.ma.median(ma_ranked, axis=0).filled(np.nan) @@ -160,13 +159,9 @@ def _highly_variable_genes_seurat_v3( adata.var['highly_variable_rank'] = df['highly_variable_rank'].values adata.var['means'] = df['means'].values adata.var['variances'] = df['variances'].values - adata.var['variances_norm'] = df['variances_norm'].values.astype( - 'float64', copy=False - ) + adata.var['variances_norm'] = df['variances_norm'].values.astype('float64', copy=False) if batch_key is not None: - adata.var['highly_variable_nbatches'] = df[ - 'highly_variable_nbatches' - ].values + adata.var['highly_variable_nbatches'] = df['highly_variable_nbatches'].values if subset: adata._inplace_subset_var(df['highly_variable'].values) else: @@ -230,14 +225,11 @@ def _highly_variable_genes_single_batch( ) # Circumvent pandas 0.23 bug. Both sides of the assignment have dtype==float32, # but there’s still a dtype error without “.value”. - disp_std_bin[one_gene_per_bin.values] = disp_mean_bin[ - one_gene_per_bin.values - ].values + disp_std_bin[one_gene_per_bin.values] = disp_mean_bin[one_gene_per_bin.values].values disp_mean_bin[one_gene_per_bin.values] = 0 # actually do the normalization df['dispersions_norm'] = ( - df['dispersions'].values # use values here as index differs - - disp_mean_bin[df['mean_bin'].values].values + df['dispersions'].values - disp_mean_bin[df['mean_bin'].values].values # use values here as index differs ) / disp_std_bin[df['mean_bin'].values].values elif flavor == 'cell_ranger': from statsmodels import robust @@ -260,18 +252,13 @@ def _highly_variable_genes_single_batch( dispersion_norm = df['dispersions_norm'].values if n_top_genes is not None: dispersion_norm = dispersion_norm[~np.isnan(dispersion_norm)] - dispersion_norm[ - ::-1 - ].sort() # interestingly, np.argpartition is slightly slower + dispersion_norm[::-1].sort() # interestingly, np.argpartition is slightly slower if n_top_genes > adata.n_vars: logg.info('`n_top_genes` > `adata.n_var`, returning all genes.') n_top_genes = adata.n_vars disp_cut_off = dispersion_norm[n_top_genes - 1] gene_subset = np.nan_to_num(df['dispersions_norm'].values) >= disp_cut_off - logg.debug( - f'the {n_top_genes} top genes correspond to a ' - f'normalized dispersion cutoff of {disp_cut_off}' - ) + logg.debug(f'the {n_top_genes} top genes correspond to a ' f'normalized dispersion cutoff of {disp_cut_off}') else: dispersion_norm[np.isnan(dispersion_norm)] = 0 # similar to Seurat gene_subset = np.logical_and.reduce( @@ -403,9 +390,7 @@ def highly_variable_genes( This function replaces :func:`~scanpy.pp.filter_genes_dispersion`. """ - if n_top_genes is not None and not all( - m is None for m in [min_disp, max_disp, min_mean, max_mean] - ): + if n_top_genes is not None and not all(m is None for m in [min_disp, max_disp, min_mean, max_mean]): logg.info('If you pass `n_top_genes`, all cutoffs are ignored.') start = logg.info('extracting highly variable genes') @@ -491,12 +476,8 @@ def highly_variable_genes( highly_variable=np.nansum, ) ) - df.rename( - columns=dict(highly_variable='highly_variable_nbatches'), inplace=True - ) - df['highly_variable_intersection'] = df['highly_variable_nbatches'] == len( - batches - ) + df.rename(columns=dict(highly_variable='highly_variable_nbatches'), inplace=True) + df['highly_variable_intersection'] = df['highly_variable_nbatches'] == len(batches) if n_top_genes is not None: # sort genes by how often they selected as hvg within each batch and @@ -538,16 +519,10 @@ def highly_variable_genes( adata.var['highly_variable'] = df['highly_variable'].values adata.var['means'] = df['means'].values adata.var['dispersions'] = df['dispersions'].values - adata.var['dispersions_norm'] = df['dispersions_norm'].values.astype( - 'float32', copy=False - ) + adata.var['dispersions_norm'] = df['dispersions_norm'].values.astype('float32', copy=False) if batch_key is not None: - adata.var['highly_variable_nbatches'] = df[ - 'highly_variable_nbatches' - ].values - adata.var['highly_variable_intersection'] = df[ - 'highly_variable_intersection' - ].values + adata.var['highly_variable_nbatches'] = df['highly_variable_nbatches'].values + adata.var['highly_variable_intersection'] = df['highly_variable_intersection'].values if subset: adata._inplace_subset_var(df['highly_variable'].values) else: diff --git a/scanpy/preprocessing/_normalization.py b/scanpy/preprocessing/_normalization.py index be78bdb0c8..569976f361 100644 --- a/scanpy/preprocessing/_normalization.py +++ b/scanpy/preprocessing/_normalization.py @@ -148,9 +148,7 @@ def normalize_total( if layers == 'all': layers = adata.layers.keys() elif isinstance(layers, str): - raise ValueError( - f"`layers` needs to be a list of strings or 'all', not {layers!r}" - ) + raise ValueError(f"`layers` needs to be a list of strings or 'all', not {layers!r}") view_to_actual(adata) @@ -216,9 +214,7 @@ def normalize_total( time=start, ) if key_added is not None: - logg.debug( - f'and added {key_added!r}, counts per cell before normalization (adata.obs)' - ) + logg.debug(f'and added {key_added!r}, counts per cell before normalization (adata.obs)') if copy: return adata diff --git a/scanpy/preprocessing/_pca.py b/scanpy/preprocessing/_pca.py index fc4ec679cc..12a03b60a9 100644 --- a/scanpy/preprocessing/_pca.py +++ b/scanpy/preprocessing/_pca.py @@ -138,9 +138,7 @@ def pca( use_highly_variable = True if 'highly_variable' in adata.var.keys() else False if use_highly_variable: logg.info(' on highly variable genes') - adata_comp = ( - adata[:, adata.var['highly_variable']] if use_highly_variable else adata - ) + adata_comp = adata[:, adata.var['highly_variable']] if use_highly_variable else adata if n_comps is None: min_dim = min(adata_comp.n_vars, adata_comp.n_obs) @@ -182,9 +180,7 @@ def pca( "This may take a very large amount of memory." ) X = X.toarray() - pca_ = PCA( - n_components=n_comps, svd_solver=svd_solver, random_state=random_state - ) + pca_ = PCA(n_components=n_comps, svd_solver=svd_solver, random_state=random_state) X_pca = pca_.fit_transform(X) elif issparse(X) and zero_center: from sklearn.decomposition import PCA @@ -197,9 +193,7 @@ def pca( 'Use "arpack" (the default) or "lobpcg" instead.' ) - output = _pca_with_sparse( - X, n_comps, solver=svd_solver, random_state=random_state - ) + output = _pca_with_sparse(X, n_comps, solver=svd_solver, random_state=random_state) # this is just a wrapper for the results X_pca = output['X_pca'] pca_ = PCA(n_components=n_comps, svd_solver=svd_solver) @@ -215,9 +209,7 @@ def pca( ' the first component, e.g., might be heavily influenced by different means\n' ' the following components often resemble the exact PCA very closely' ) - pca_ = TruncatedSVD( - n_components=n_comps, random_state=random_state, algorithm=svd_solver - ) + pca_ = TruncatedSVD(n_components=n_comps, random_state=random_state, algorithm=svd_solver) X_pca = pca_.fit_transform(X) else: raise Exception("This shouldn't happen. Please open a bug report.") diff --git a/scanpy/preprocessing/_qc.py b/scanpy/preprocessing/_qc.py index 1f966f9d96..f915fe738c 100644 --- a/scanpy/preprocessing/_qc.py +++ b/scanpy/preprocessing/_qc.py @@ -24,8 +24,7 @@ def _choose_mtx_rep(adata, use_raw=False, layer=None): is_layer = layer is not None if use_raw and is_layer: raise ValueError( - "Cannot use expression from both layer and raw. You provided:" - f"'use_raw={use_raw}' and 'layer={layer}'" + "Cannot use expression from both layer and raw. You provided:" f"'use_raw={use_raw}' and 'layer={layer}'" ) if is_layer: return adata.layers[layer] @@ -103,33 +102,21 @@ def describe_obs( else: obs_metrics[f"n_{var_type}_by_{expr_type}"] = np.count_nonzero(X, axis=1) if log1p: - obs_metrics[f"log1p_n_{var_type}_by_{expr_type}"] = np.log1p( - obs_metrics[f"n_{var_type}_by_{expr_type}"] - ) + obs_metrics[f"log1p_n_{var_type}_by_{expr_type}"] = np.log1p(obs_metrics[f"n_{var_type}_by_{expr_type}"]) obs_metrics[f"total_{expr_type}"] = X.sum(axis=1) if log1p: - obs_metrics[f"log1p_total_{expr_type}"] = np.log1p( - obs_metrics[f"total_{expr_type}"] - ) + obs_metrics[f"log1p_total_{expr_type}"] = np.log1p(obs_metrics[f"total_{expr_type}"]) if percent_top: percent_top = sorted(percent_top) proportions = top_segment_proportions(X, percent_top) for i, n in enumerate(percent_top): - obs_metrics[f"pct_{expr_type}_in_top_{n}_{var_type}"] = ( - proportions[:, i] * 100 - ) + obs_metrics[f"pct_{expr_type}_in_top_{n}_{var_type}"] = proportions[:, i] * 100 for qc_var in qc_vars: - obs_metrics[f"total_{expr_type}_{qc_var}"] = X[:, adata.var[qc_var].values].sum( - axis=1 - ) + obs_metrics[f"total_{expr_type}_{qc_var}"] = X[:, adata.var[qc_var].values].sum(axis=1) if log1p: - obs_metrics[f"log1p_total_{expr_type}_{qc_var}"] = np.log1p( - obs_metrics[f"total_{expr_type}_{qc_var}"] - ) + obs_metrics[f"log1p_total_{expr_type}_{qc_var}"] = np.log1p(obs_metrics[f"total_{expr_type}_{qc_var}"]) obs_metrics[f"pct_{expr_type}_{qc_var}"] = ( - obs_metrics[f"total_{expr_type}_{qc_var}"] - / obs_metrics[f"total_{expr_type}"] - * 100 + obs_metrics[f"total_{expr_type}_{qc_var}"] / obs_metrics[f"total_{expr_type}"] * 100 ) if inplace: adata.obs[obs_metrics.columns] = obs_metrics @@ -193,17 +180,11 @@ def describe_var( var_metrics["n_cells_by_{expr_type}"] = np.count_nonzero(X, axis=0) var_metrics["mean_{expr_type}"] = X.mean(axis=0) if log1p: - var_metrics["log1p_mean_{expr_type}"] = np.log1p( - var_metrics["mean_{expr_type}"] - ) - var_metrics["pct_dropout_by_{expr_type}"] = ( - 1 - var_metrics["n_cells_by_{expr_type}"] / X.shape[0] - ) * 100 + var_metrics["log1p_mean_{expr_type}"] = np.log1p(var_metrics["mean_{expr_type}"]) + var_metrics["pct_dropout_by_{expr_type}"] = (1 - var_metrics["n_cells_by_{expr_type}"] / X.shape[0]) * 100 var_metrics["total_{expr_type}"] = np.ravel(X.sum(axis=0)) if log1p: - var_metrics["log1p_total_{expr_type}"] = np.log1p( - var_metrics["total_{expr_type}"] - ) + var_metrics["log1p_total_{expr_type}"] = np.log1p(var_metrics["total_{expr_type}"]) # Relabel new_colnames = [] for col in var_metrics.columns: @@ -377,9 +358,7 @@ def top_proportions_sparse_csr(data, indptr, n): return values -def top_segment_proportions( - mtx: Union[np.array, spmatrix], ns: Collection[int] -) -> np.ndarray: +def top_segment_proportions(mtx: Union[np.array, spmatrix], ns: Collection[int]) -> np.ndarray: """ Calculates total percentage of counts in top ns genes. @@ -403,15 +382,11 @@ def top_segment_proportions( return top_segment_proportions_dense(mtx, ns) -def top_segment_proportions_dense( - mtx: Union[np.array, spmatrix], ns: Collection[int] -) -> np.ndarray: +def top_segment_proportions_dense(mtx: Union[np.array, spmatrix], ns: Collection[int]) -> np.ndarray: # Currently ns is considered to be 1 indexed ns = np.sort(ns) sums = mtx.sum(axis=1) - partitioned = np.apply_along_axis(np.partition, 1, mtx, mtx.shape[1] - ns)[:, ::-1][ - :, : ns[-1] - ] + partitioned = np.apply_along_axis(np.partition, 1, mtx, mtx.shape[1] - ns)[:, ::-1][:, : ns[-1]] values = np.zeros((mtx.shape[0], len(ns))) acc = np.zeros(mtx.shape[0]) prev = 0 diff --git a/scanpy/preprocessing/_recipes.py b/scanpy/preprocessing/_recipes.py index d211bcc20a..8d6f43b364 100644 --- a/scanpy/preprocessing/_recipes.py +++ b/scanpy/preprocessing/_recipes.py @@ -47,9 +47,7 @@ def recipe_weinreb17( adata = adata.copy() if log: pp.log1p(adata) - adata.X = normalize_per_cell_weinreb16_deprecated( - adata.X, max_fraction=0.05, mult_with_mean=True - ) + adata.X = normalize_per_cell_weinreb16_deprecated(adata.X, max_fraction=0.05, mult_with_mean=True) gene_subset = filter_genes_cv_deprecated(adata.X, mean_threshold, cv_threshold) adata._inplace_subset_var(gene_subset) # this modifies the object itself X_pca = pp.pca( @@ -63,9 +61,7 @@ def recipe_weinreb17( return adata if copy else None -def recipe_seurat( - adata: AnnData, log: bool = True, plot: bool = False, copy: bool = False -) -> Optional[AnnData]: +def recipe_seurat(adata: AnnData, log: bool = True, plot: bool = False, copy: bool = False) -> Optional[AnnData]: """\ Normalization and filtering as of Seurat [Satija15]_. @@ -79,9 +75,7 @@ def recipe_seurat( pp.filter_cells(adata, min_genes=200) pp.filter_genes(adata, min_cells=3) normalize_total(adata, target_sum=1e4) - filter_result = filter_genes_dispersion( - adata.X, min_mean=0.0125, max_mean=3, min_disp=0.5, log=not log - ) + filter_result = filter_genes_dispersion(adata.X, min_mean=0.0125, max_mean=3, min_disp=0.5, log=not log) if plot: from ..plotting import ( _preprocessing as ppp, @@ -152,9 +146,7 @@ def recipe_zheng17( pp.filter_genes(adata, min_counts=1) # normalize with total UMI count per cell normalize_total(adata, key_added='n_counts_all') - filter_result = filter_genes_dispersion( - adata.X, flavor='cell_ranger', n_top_genes=n_top_genes, log=False - ) + filter_result = filter_genes_dispersion(adata.X, flavor='cell_ranger', n_top_genes=n_top_genes, log=False) if plot: # should not import at the top of the file from ..plotting import _preprocessing as ppp diff --git a/scanpy/preprocessing/_simple.py b/scanpy/preprocessing/_simple.py index 344ea2022c..3995aad9d7 100644 --- a/scanpy/preprocessing/_simple.py +++ b/scanpy/preprocessing/_simple.py @@ -119,9 +119,7 @@ def filter_cells( """ if copy: logg.warning('`copy` is deprecated, use `inplace` instead.') - n_given_options = sum( - option is not None for option in [min_genes, min_counts, max_genes, max_counts] - ) + n_given_options = sum(option is not None for option in [min_genes, min_counts, max_genes, max_counts]) if n_given_options != 1: raise ValueError( 'Only provide one of the optional parameters `min_counts`, ' @@ -143,9 +141,7 @@ def filter_cells( X = data # proceed with processing the data matrix min_number = min_counts if min_genes is None else min_genes max_number = max_counts if max_genes is None else max_genes - number_per_cell = np.sum( - X if min_genes is None and max_genes is None else X > 0, axis=1 - ) + number_per_cell = np.sum(X if min_genes is None and max_genes is None else X > 0, axis=1) if issparse(X): number_per_cell = number_per_cell.A1 if min_number is not None: @@ -158,18 +154,10 @@ def filter_cells( msg = f'filtered out {s} cells that have ' if min_genes is not None or min_counts is not None: msg += 'less than ' - msg += ( - f'{min_genes} genes expressed' - if min_counts is None - else f'{min_counts} counts' - ) + msg += f'{min_genes} genes expressed' if min_counts is None else f'{min_counts} counts' if max_genes is not None or max_counts is not None: msg += 'more than ' - msg += ( - f'{max_genes} genes expressed' - if max_counts is None - else f'{max_counts} counts' - ) + msg += f'{max_genes} genes expressed' if max_counts is None else f'{max_counts} counts' logg.info(msg) return cell_subset, number_per_cell @@ -223,9 +211,7 @@ def filter_genes( """ if copy: logg.warning('`copy` is deprecated, use `inplace` instead.') - n_given_options = sum( - option is not None for option in [min_cells, min_counts, max_cells, max_counts] - ) + n_given_options = sum(option is not None for option in [min_cells, min_counts, max_cells, max_counts]) if n_given_options != 1: raise ValueError( 'Only provide one of the optional parameters `min_counts`, ' @@ -255,9 +241,7 @@ def filter_genes( X = data # proceed with processing the data matrix min_number = min_counts if min_cells is None else min_cells max_number = max_counts if max_cells is None else max_cells - number_per_gene = np.sum( - X if min_cells is None and max_cells is None else X > 0, axis=0 - ) + number_per_gene = np.sum(X if min_cells is None and max_cells is None else X > 0, axis=0) if issparse(X): number_per_gene = number_per_gene.A1 if min_number is not None: @@ -270,14 +254,10 @@ def filter_genes( msg = f'filtered out {s} genes that are detected ' if min_cells is not None or min_counts is not None: msg += 'in less than ' - msg += ( - f'{min_cells} cells' if min_counts is None else f'{min_counts} counts' - ) + msg += f'{min_cells} cells' if min_counts is None else f'{min_counts} counts' if max_cells is not None or max_counts is not None: msg += 'in more than ' - msg += ( - f'{max_cells} cells' if max_counts is None else f'{max_counts} counts' - ) + msg += f'{max_cells} cells' if max_counts is None else f'{max_counts} counts' logg.info(msg) return gene_subset, number_per_gene @@ -323,17 +303,13 @@ def log1p( ------- Returns or updates `data`, depending on `copy`. """ - _check_array_function_arguments( - chunked=chunked, chunk_size=chunk_size, layer=layer, obsm=obsm - ) + _check_array_function_arguments(chunked=chunked, chunk_size=chunk_size, layer=layer, obsm=obsm) return log1p_array(X, copy=copy, base=base) @log1p.register(spmatrix) def log1p_sparse(X, *, base: Optional[Number] = None, copy: bool = False): - X = check_array( - X, accept_sparse=("csr", "csc"), dtype=(np.float64, np.float32), copy=copy - ) + X = check_array(X, accept_sparse=("csr", "csc"), dtype=(np.float64, np.float32), copy=copy) X.data = log1p(X.data, copy=False, base=base) return X @@ -347,9 +323,7 @@ def log1p_array(X, *, base: Optional[Number] = None, copy: bool = False): X = X.astype(np.floating) else: X = X.copy() - elif not ( - np.issubdtype(X.dtype, np.floating) or np.issubdtype(X.dtype, np.complex) - ): + elif not (np.issubdtype(X.dtype, np.floating) or np.issubdtype(X.dtype, np.complex)): X = X.astype(np.floating) np.log1p(X, out=X) if base is not None: @@ -376,9 +350,7 @@ def log1p_anndata( if chunked: if (layer is not None) or (obsm is not None): - raise NotImplementedError( - "Currently cannot perform chunked operations on arrays not stored in X." - ) + raise NotImplementedError("Currently cannot perform chunked operations on arrays not stored in X.") for chunk, start, end in adata.chunked_X(chunk_size): adata.X[start:end] = log1p(chunk, base=base, copy=False) else: @@ -520,9 +492,7 @@ def normalize_per_cell( start = logg.info('normalizing by total count per cell') adata = data.copy() if copy else data if counts_per_cell is None: - cell_subset, counts_per_cell = materialize_as_ndarray( - filter_cells(adata.X, min_counts=min_counts) - ) + cell_subset, counts_per_cell = materialize_as_ndarray(filter_cells(adata.X, min_counts=min_counts)) adata.obs[key_n_counts] = counts_per_cell adata._inplace_subset_obs(cell_subset) counts_per_cell = counts_per_cell[cell_subset] @@ -698,9 +668,7 @@ def _regress_out_chunk(data): else: regres = regressors try: - result = sm.GLM( - data_chunk[:, col_index], regres, family=sm.families.Gaussian() - ).fit() + result = sm.GLM(data_chunk[:, col_index], regres, family=sm.families.Gaussian()).fit() new_column = result.resid_response except PerfectSeparationError: # this emulates R's behavior logg.warning('Encountered PerfectSeparationError, setting to 0 as in R.') @@ -752,9 +720,13 @@ def scale( annotated with `'mean'` and `'std'` in `adata.var`. """ _check_array_function_arguments(layer=layer, obsm=obsm) +<<<<<<< HEAD return scale_array( data, zero_center=zero_center, max_value=max_value, copy=copy # noqa: F821 ) +======= + return scale_array(data, zero_center=zero_center, max_value=max_value, copy=copy) # noqa: F821 +>>>>>>> 7a096bf9 (add flake8 pre-commit) @scale.register(np.ndarray) @@ -774,10 +746,7 @@ def scale_array( ) if np.issubdtype(X.dtype, np.integer): - logg.info( - '... as scaling leads to float results, integer ' - 'input is cast to float, returning copy.' - ) + logg.info('... as scaling leads to float results, integer ' 'input is cast to float, returning copy.') X = X.astype(float) mean, var = _get_mean_var(X) @@ -814,10 +783,7 @@ def scale_sparse( ): # need to add the following here to make inplace logic work if zero_center: - logg.info( - "... as `zero_center=True`, sparse input is " - "densified and may lead to large memory consumption" - ) + logg.info("... as `zero_center=True`, sparse input is " "densified and may lead to large memory consumption") X = X.toarray() copy = False # Since the data has been copied return scale_array( @@ -951,9 +917,7 @@ def downsample_counts( total_counts_call = total_counts is not None counts_per_cell_call = counts_per_cell is not None if total_counts_call is counts_per_cell_call: - raise ValueError( - "Must specify exactly one of `total_counts` or `counts_per_cell`." - ) + raise ValueError("Must specify exactly one of `total_counts` or `counts_per_cell`.") if copy: adata = adata.copy() if total_counts_call: diff --git a/scanpy/preprocessing/_utils.py b/scanpy/preprocessing/_utils.py index 45ec781661..303a4d58d2 100644 --- a/scanpy/preprocessing/_utils.py +++ b/scanpy/preprocessing/_utils.py @@ -35,9 +35,7 @@ def sparse_mean_variance_axis(mtx: sparse.spmatrix, axis: int): else: raise ValueError("This function only works on sparse csr and csc matrices") if axis == ax_minor: - return sparse_mean_var_major_axis( - mtx.data, mtx.indices, mtx.indptr, *shape, np.float64 - ) + return sparse_mean_var_major_axis(mtx.data, mtx.indices, mtx.indptr, *shape, np.float64) else: return sparse_mean_var_minor_axis(mtx.data, mtx.indices, *shape, np.float64) diff --git a/scanpy/queries/_queries.py b/scanpy/queries/_queries.py index 7dff67565f..c206a5262e 100644 --- a/scanpy/queries/_queries.py +++ b/scanpy/queries/_queries.py @@ -60,13 +60,9 @@ def simple_query( try: from pybiomart import Server except ImportError: - raise ImportError( - "This method requires the `pybiomart` module to be installed." - ) + raise ImportError("This method requires the `pybiomart` module to be installed.") server = Server(host, use_cache=use_cache) - dataset = server.marts["ENSEMBL_MART_ENSEMBL"].datasets[ - "{}_gene_ensembl".format(org) - ] + dataset = server.marts["ENSEMBL_MART_ENSEMBL"].datasets["{}_gene_ensembl".format(org)] res = dataset.query(attributes=attrs, filters=filters, use_attr_names=True) return res @@ -264,16 +260,13 @@ def enrich( try: from gprofiler import GProfiler except ImportError: - raise ImportError( - "This method requires the `gprofiler-official` module to be installed." - ) + raise ImportError("This method requires the `gprofiler-official` module to be installed.") gprofiler = GProfiler(user_agent="scanpy", return_dataframe=True) gprofiler_kwargs = dict(gprofiler_kwargs) for k in ["organism"]: if gprofiler_kwargs.get(k) is not None: raise ValueError( - f"Argument `{k}` should be passed directly through `enrich`, " - "not through `gprofiler_kwargs`" + f"Argument `{k}` should be passed directly through `enrich`, " "not through `gprofiler_kwargs`" ) return gprofiler.profile(container, organism=org, **gprofiler_kwargs) diff --git a/scanpy/readwrite.py b/scanpy/readwrite.py index 87d2055fc4..c31f8f042f 100644 --- a/scanpy/readwrite.py +++ b/scanpy/readwrite.py @@ -214,8 +214,7 @@ def _read_legacy_10x_h5(filename, *, genome=None, start=None): genome = children[0] elif genome not in children: raise ValueError( - f"Could not find genome '{genome}' in '{filename}'. " - f'Available genomes are: {children}' + f"Could not find genome '{genome}' in '{filename}'. " f'Available genomes are: {children}' ) dsets = {} for node in f.walk_nodes('/' + genome, 'Array'): @@ -373,26 +372,19 @@ def read_visium( for f in files.values(): if not f.exists(): if any(x in str(f) for x in ["hires_image", "lowres_image"]): - logg.warning( - f"You seem to be missing an image file.\n" - f"Could not find '{f}'." - ) + logg.warning(f"You seem to be missing an image file.\n" f"Could not find '{f}'.") else: raise OSError(f"Could not find '{f}'") adata.uns["spatial"][library_id]['images'] = dict() for res in ['hires', 'lowres']: try: - adata.uns["spatial"][library_id]['images'][res] = imread( - str(files[f'{res}_image']) - ) + adata.uns["spatial"][library_id]['images'][res] = imread(str(files[f'{res}_image'])) except Exception: raise OSError(f"Could not find '{res}_image'") # read json scalefactors - adata.uns["spatial"][library_id]['scalefactors'] = json.loads( - files['scalefactors_json_file'].read_bytes() - ) + adata.uns["spatial"][library_id]['scalefactors'] = json.loads(files['scalefactors_json_file'].read_bytes()) adata.uns["spatial"][library_id]["metadata"] = { k: (str(attrs[k], "utf-8") if isinstance(attrs[k], bytes) else attrs[k]) @@ -414,9 +406,7 @@ def read_visium( adata.obs = adata.obs.join(positions, how="left") - adata.obsm['spatial'] = adata.obs[ - ['pxl_row_in_fullres', 'pxl_col_in_fullres'] - ].to_numpy() + adata.obsm['spatial'] = adata.obs[['pxl_row_in_fullres', 'pxl_col_in_fullres']].to_numpy() adata.obs.drop( columns=['barcode', 'pxl_row_in_fullres', 'pxl_col_in_fullres'], inplace=True, @@ -426,9 +416,7 @@ def read_visium( if source_image_path is not None: # get an absolute path source_image_path = str(Path(source_image_path).resolve()) - adata.uns["spatial"][library_id]["metadata"]["source_image_path"] = str( - source_image_path - ) + adata.uns["spatial"][library_id]["metadata"]["source_image_path"] = str(source_image_path) return adata @@ -489,9 +477,7 @@ def read_10x_mtx( if genefile_exists or not gex_only: return adata else: - gex_rows = list( - map(lambda x: x == 'Gene Expression', adata.var['feature_types']) - ) + gex_rows = list(map(lambda x: x == 'Gene Expression', adata.var['feature_types'])) return adata[:, gex_rows].copy() @@ -560,9 +546,7 @@ def _read_v3_10x_mtx( else: raise ValueError("`var_names` needs to be 'gene_symbols' or 'gene_ids'") adata.var['feature_types'] = genes[2].values - adata.obs_names = pd.read_csv(path / f'{prefix}barcodes.tsv.gz', header=None)[ - 0 - ].values + adata.obs_names = pd.read_csv(path / f'{prefix}barcodes.tsv.gz', header=None)[0].values return adata @@ -612,9 +596,7 @@ def write( if ext == 'csv': adata.write_csvs(filename) else: - adata.write( - filename, compression=compression, compression_opts=compression_opts - ) + adata.write(filename, compression=compression, compression_opts=compression_opts) # ------------------------------------------------------------------------------- @@ -622,9 +604,7 @@ def write( # ------------------------------------------------------------------------------- -def read_params( - filename: Union[Path, str], asheader: bool = False -) -> Dict[str, Union[int, float, bool, str, None]]: +def read_params(filename: Union[Path, str], asheader: bool = False) -> Dict[str, Union[int, float, bool, str, None]]: """\ Read parameter dictionary from text file. @@ -699,9 +679,7 @@ def _read( **kwargs, ): if ext is not None and ext not in avail_exts: - raise ValueError( - 'Please provide one of the available extensions.\n' f'{avail_exts}' - ) + raise ValueError('Please provide one of the available extensions.\n' f'{avail_exts}') else: ext = is_valid_filename(filename, return_ext=True) is_present = _check_datafile_present_and_download(filename, backup_url=backup_url) @@ -715,9 +693,7 @@ def _read( logg.debug(f'reading sheet {sheet} from file {filename}') return read_hdf(filename, sheet) # read other file types - path_cache = settings.cachedir / _slugify(filename).replace( - '.' + ext, '.h5ad' - ) # type: Path + path_cache = settings.cachedir / _slugify(filename).replace('.' + ext, '.h5ad') # type: Path if path_cache.suffix in {'.gz', '.bz2'}: path_cache = path_cache.with_suffix('') if cache and path_cache.is_file(): @@ -756,10 +732,7 @@ def _read( else: raise ValueError(f'Unknown extension {ext}.') if cache: - logg.info( - f'... writing an {settings.file_format_data} ' - 'cache file to speedup reading next time' - ) + logg.info(f'... writing an {settings.file_format_data} ' 'cache file to speedup reading next time') if cache_compression is _empty: cache_compression = settings.cache_compression if not path_cache.parent.is_dir(): @@ -903,9 +876,7 @@ def get_used_files(): """Get files used by processes with name scanpy.""" import psutil - loop_over_scanpy_processes = ( - proc for proc in psutil.process_iter() if proc.name() == 'scanpy' - ) + loop_over_scanpy_processes = (proc for proc in psutil.process_iter() if proc.name() == 'scanpy') filenames = [] for proc in loop_over_scanpy_processes: try: @@ -967,10 +938,7 @@ def _check_datafile_present_and_download(path, backup_url=None): return True if backup_url is None: return False - logg.info( - f'try downloading from url\n{backup_url}\n' - '... this may take a while but only happens once' - ) + logg.info(f'try downloading from url\n{backup_url}\n' '... this may take a while but only happens once') if not path.parent.is_dir(): logg.info(f'creating directory {path.parent}/ for saving data') path.parent.mkdir(parents=True) @@ -985,8 +953,7 @@ def is_valid_filename(filename: Path, return_ext=False): if len(ext) > 2: logg.warning( - f'Your filename has more than two extensions: {ext}.\n' - f'Only considering the two last: {ext[-2:]}.' + f'Your filename has more than two extensions: {ext}.\n' f'Only considering the two last: {ext[-2:]}.' ) ext = ext[-2:] diff --git a/scanpy/tests/conftest.py b/scanpy/tests/conftest.py index 574dbf3543..01c87d95c0 100644 --- a/scanpy/tests/conftest.py +++ b/scanpy/tests/conftest.py @@ -1,14 +1,14 @@ +import scanpy +import pytest +from matplotlib.testing.compare import compare_images, make_test_filename +from matplotlib import pyplot import sys from pathlib import Path import matplotlib as mpl mpl.use('agg') -from matplotlib import pyplot -from matplotlib.testing.compare import compare_images, make_test_filename -import pytest -import scanpy scanpy.settings.verbosity = "hint" diff --git a/scanpy/tests/external/test_hashsolo.py b/scanpy/tests/external/test_hashsolo.py index 8ab8df0e61..4d3a223f28 100644 --- a/scanpy/tests/external/test_hashsolo.py +++ b/scanpy/tests/external/test_hashsolo.py @@ -23,9 +23,7 @@ def test_cell_demultiplexing(): sce.pp.hashsolo(test_data, test_data.obs.columns) doublets = ["Doublet"] * 10 - classes = list( - np.repeat(np.arange(10), 98).reshape(98, 10, order="F").ravel().astype(str) - ) + classes = list(np.repeat(np.arange(10), 98).reshape(98, 10, order="F").ravel().astype(str)) negatives = ["Negative"] * 10 classification = doublets + classes + negatives assert all(test_data.obs["Classification"].astype(str) == classification) diff --git a/scanpy/tests/external/test_wishbone.py b/scanpy/tests/external/test_wishbone.py index fc3cf71901..9baca5f877 100644 --- a/scanpy/tests/external/test_wishbone.py +++ b/scanpy/tests/external/test_wishbone.py @@ -20,6 +20,4 @@ def test_run_wishbone(): components=[2, 3], num_waypoints=150, ) - assert all( - [k in adata.obs for k in ['trajectory_wishbone', 'branch_wishbone']] - ), "Run Wishbone Error!" + assert all([k in adata.obs for k in ['trajectory_wishbone', 'branch_wishbone']]), "Run Wishbone Error!" diff --git a/scanpy/tests/helpers.py b/scanpy/tests/helpers.py index 61fc35e23e..c2d4219fdd 100644 --- a/scanpy/tests/helpers.py +++ b/scanpy/tests/helpers.py @@ -37,6 +37,7 @@ def check_rep_mutation(func, X, *, fields=["layer", "obsm"], **kwargs): ) np.testing.assert_array_equal(asarray(adata_X.X), result_array) +<<<<<<< HEAD # Unmodified fields for field in fields: np.testing.assert_array_equal(X_array, asarray(adatas_proc[field].X)) @@ -48,6 +49,11 @@ def check_rep_mutation(func, X, *, fields=["layer", "obsm"], **kwargs): sc.get._get_obs_rep(adatas_proc[field_a], **{field_b: field_b}) ) np.testing.assert_array_equal(X_array, result_array) +======= + assert np.array_equal(asarray(adata_layer.X), asarray(adata_layer.obsm["obsm"])) + assert np.array_equal(asarray(adata_obsm.X), asarray(adata_obsm.layers["layer"])) + assert np.array_equal(asarray(adata_X.layers["layer"]), asarray(adata_X.obsm["obsm"])) +>>>>>>> 40dc2c3b (add flake8 pre-commit) def check_rep_results(func, X, *, fields=["layer", "obsm"], **kwargs): diff --git a/scanpy/tests/notebooks/test_paga_paul15_subsampled.py b/scanpy/tests/notebooks/test_paga_paul15_subsampled.py index 839d93f40a..38c3b4f7d3 100644 --- a/scanpy/tests/notebooks/test_paga_paul15_subsampled.py +++ b/scanpy/tests/notebooks/test_paga_paul15_subsampled.py @@ -3,6 +3,7 @@ # # This is the subsampled notebook for testing. +import scanpy as sc from pathlib import Path import numpy as np @@ -10,8 +11,6 @@ setup() -import scanpy as sc - HERE: Path = Path(__file__).parent ROOT = HERE / '_images_paga_paul15_subsampled' @@ -115,9 +114,7 @@ def test_paga_paul15_subsampled(image_comparer, plt): adata.obs['distance'] = adata.obs['dpt_pseudotime'] - _, axs = plt.subplots( - ncols=3, figsize=(6, 2.5), gridspec_kw={'wspace': 0.05, 'left': 0.12} - ) + _, axs = plt.subplots(ncols=3, figsize=(6, 2.5), gridspec_kw={'wspace': 0.05, 'left': 0.12}) plt.subplots_adjust(left=0.05, right=0.98, top=0.82, bottom=0.2) for ipath, (descr, path) in enumerate(paths): _, data = sc.pl.paga_path( diff --git a/scanpy/tests/notebooks/test_pbmc3k.py b/scanpy/tests/notebooks/test_pbmc3k.py index 0146c4b4de..42bd171986 100644 --- a/scanpy/tests/notebooks/test_pbmc3k.py +++ b/scanpy/tests/notebooks/test_pbmc3k.py @@ -10,6 +10,7 @@ # ([here](http://cf.10xgenomics.com/samples/cell-exp/1.1.0/pbmc3k/pbmc3k_filtered_gene_bc_matrices.tar.gz) # from this [webpage](https://support.10xgenomics.com/single-cell-gene-expression/datasets/1.1.0/pbmc3k)). +import scanpy as sc from pathlib import Path import numpy as np @@ -19,8 +20,6 @@ setup() -import scanpy as sc - HERE: Path = Path(__file__).parent ROOT = HERE / 'pbmc3k_images' @@ -30,9 +29,7 @@ def test_pbmc3k(image_comparer): save_and_compare_images = image_comparer(ROOT, FIGS, tol=20) - adata = sc.read( - './data/pbmc3k_raw.h5ad', backup_url='http://falexwolf.de/data/pbmc3k_raw.h5ad' - ) + adata = sc.read('./data/pbmc3k_raw.h5ad', backup_url='http://falexwolf.de/data/pbmc3k_raw.h5ad') # Preprocessing @@ -45,9 +42,7 @@ def test_pbmc3k(image_comparer): mito_genes = [name for name in adata.var_names if name.startswith('MT-')] # for each cell compute fraction of counts in mito genes vs. all genes # the `.A1` is only necessary as X is sparse to transform to a dense array after summing - adata.obs['percent_mito'] = ( - np.sum(adata[:, mito_genes].X, axis=1).A1 / np.sum(adata.X, axis=1).A1 - ) + adata.obs['percent_mito'] = np.sum(adata[:, mito_genes].X, axis=1).A1 / np.sum(adata.X, axis=1).A1 # add the total counts per cell as observations-annotation to adata adata.obs['n_counts'] = adata.X.sum(axis=1).A1 @@ -144,7 +139,5 @@ def test_pbmc3k(image_comparer): # sc.pl.umap(adata, color='louvain', legend_loc='on data', title='', frameon=False, show=False) # save_and_compare_images('umap_3') - sc.pl.violin( - adata, ['CST3', 'NKG7', 'PPBP'], groupby='louvain', rotation=90, show=False - ) + sc.pl.violin(adata, ['CST3', 'NKG7', 'PPBP'], groupby='louvain', rotation=90, show=False) save_and_compare_images('violin_2') diff --git a/scanpy/tests/test_combat.py b/scanpy/tests/test_combat.py index 295667b909..79be9a10ea 100644 --- a/scanpy/tests/test_combat.py +++ b/scanpy/tests/test_combat.py @@ -37,9 +37,7 @@ def test_covariates(): adata.obs['cat2'] = np.random.binomial(2, 0.1, size=(adata.n_obs)) adata.obs['num1'] = np.random.normal(size=(adata.n_obs)) - X2 = sc.pp.combat( - adata, key=key, covariates=['cat1', 'cat2', 'num1'], inplace=False - ) + X2 = sc.pp.combat(adata, key=key, covariates=['cat1', 'cat2', 'num1'], inplace=False) sc.pp.combat(adata, key=key, covariates=['cat1', 'cat2', 'num1'], inplace=True) assert X1.shape == X2.shape diff --git a/scanpy/tests/test_datasets.py b/scanpy/tests/test_datasets.py index 50332de6f9..30e3d497a8 100644 --- a/scanpy/tests/test_datasets.py +++ b/scanpy/tests/test_datasets.py @@ -63,10 +63,7 @@ def test_ebi_expression_atlas(tmp_dataset_dir): def test_krumsiek11(tmp_dataset_dir): adata = sc.datasets.krumsiek11() assert adata.shape == (640, 11) - assert all( - np.unique(adata.obs["cell_type"]) - == np.array(["Ery", "Mk", "Mo", "Neu", "progenitor"]) - ) + assert all(np.unique(adata.obs["cell_type"]) == np.array(["Ery", "Mk", "Mo", "Neu", "progenitor"])) def test_blobs(): @@ -102,18 +99,14 @@ def test_visium_datasets(tmp_dataset_dir, tmpdir): # Test that downloading tissue image works mbrain = sc.datasets.visium_sge("V1_Adult_Mouse_Brain", include_hires_tiff=True) expected_image_path = sc.settings.datasetdir / "V1_Adult_Mouse_Brain" / "image.tif" - image_path = Path( - mbrain.uns["spatial"]["V1_Adult_Mouse_Brain"]["metadata"]["source_image_path"] - ) + image_path = Path(mbrain.uns["spatial"]["V1_Adult_Mouse_Brain"]["metadata"]["source_image_path"]) assert image_path == expected_image_path # Test that tissue image exists and is a valid image file assert image_path.exists() # Test that tissue image is a tif image file (using `file`) - process = subprocess.run( - ['file', '--mime-type', image_path], stdout=subprocess.PIPE - ) + process = subprocess.run(['file', '--mime-type', image_path], stdout=subprocess.PIPE) output = process.stdout.strip().decode() # make process output string assert output == str(image_path) + ': image/tiff' diff --git a/scanpy/tests/test_embedding_plots.py b/scanpy/tests/test_embedding_plots.py index 58db03d965..e21a9caf82 100644 --- a/scanpy/tests/test_embedding_plots.py +++ b/scanpy/tests/test_embedding_plots.py @@ -30,9 +30,7 @@ def adata(): from sklearn.cluster import DBSCAN empty_pixel = np.array([1.0, 1.0, 1.0, 0]).reshape(1, 1, -1) - image = imread( - Path(sc.__file__).parent.parent / "docs/_static/img/Scanpy_Logo_RGB.png" - ) + image = imread(Path(sc.__file__).parent.parent / "docs/_static/img/Scanpy_Logo_RGB.png") x, y = np.where(np.logical_and.reduce(~np.equal(image, empty_pixel), axis=2)) # Just using to calculate the hex coords @@ -71,9 +69,7 @@ def adata(): adata.obs["label_missing"][::2] = np.nan adata.obs["1_missing"] = adata.obs_vector("1") - adata.obs.loc[ - adata.obsm["spatial"][:, 0] < adata.obsm["spatial"][:, 0].mean(), "1_missing" - ] = np.nan + adata.obs.loc[adata.obsm["spatial"][:, 0] < adata.obsm["spatial"][:, 0].mean(), "1_missing"] = np.nan return adata @@ -161,9 +157,7 @@ def test_missing_values_categorical( legend_loc, groupsfunc, ): - save_and_compare_images = image_comparer( - MISSING_VALUES_ROOT, MISSING_VALUES_FIGS, tol=15 - ) + save_and_compare_images = image_comparer(MISSING_VALUES_ROOT, MISSING_VALUES_FIGS, tol=15) base_name = fixture_request.node.name # Passing through a dict so it's easier to use default values @@ -179,12 +173,8 @@ def test_missing_values_categorical( save_and_compare_images(base_name) -def test_missing_values_continuous( - fixture_request, image_comparer, adata, plotfunc, na_color, legend_loc, vbounds -): - save_and_compare_images = image_comparer( - MISSING_VALUES_ROOT, MISSING_VALUES_FIGS, tol=15 - ) +def test_missing_values_continuous(fixture_request, image_comparer, adata, plotfunc, na_color, legend_loc, vbounds): + save_and_compare_images = image_comparer(MISSING_VALUES_ROOT, MISSING_VALUES_FIGS, tol=15) base_name = fixture_request.node.name # Passing through a dict so it's easier to use default values @@ -268,13 +258,9 @@ def test_spatial_general(image_comparer): # general coordinates save_and_compare_images = image_comparer(ROOT, FIGS, tol=15) adata = sc.read_visium(HERE / '_data' / 'visium_data' / '1.0.0') adata.obs = adata.obs.astype({'array_row': 'str'}) - spatial_metadata = adata.uns.pop( - "spatial" - ) # spatial data don't have imgs, so remove entry from uns + spatial_metadata = adata.uns.pop("spatial") # spatial data don't have imgs, so remove entry from uns # Required argument for now - spot_size = list(spatial_metadata.values())[0]["scalefactors"][ - "spot_diameter_fullres" - ] + spot_size = list(spatial_metadata.values())[0]["scalefactors"]["spot_diameter_fullres"] sc.pl.spatial(adata, show=False, spot_size=spot_size) save_and_compare_images('master_spatial_general_nocol') @@ -347,12 +333,8 @@ def equivalent_spatial_plotters_no_img(equivalent_spatial_plotters): pytest.param({"bw": True}, id="bw"), # Shape of the image for particular fixture, should not be hardcoded like this pytest.param({"img": np.ones((774, 1755, 4)), "scale_factor": 1.0}, id="img"), - pytest.param( - {"na_color": (0, 0, 0, 0), "color": "1_missing"}, id="na_color.transparent" - ), - pytest.param( - {"na_color": "lightgray", "color": "1_missing"}, id="na_color.lightgray" - ), + pytest.param({"na_color": (0, 0, 0, 0), "color": "1_missing"}, id="na_color.transparent"), + pytest.param({"na_color": "lightgray", "color": "1_missing"}, id="na_color.lightgray"), ] ) def spatial_kwargs(request): @@ -379,9 +361,7 @@ def test_manual_equivalency(equivalent_spatial_plotters, tmpdir, spatial_kwargs) check_images(orig_pth, removed_pth, tol=1) -def test_manual_equivalency_no_img( - equivalent_spatial_plotters_no_img, tmpdir, spatial_kwargs -): +def test_manual_equivalency_no_img(equivalent_spatial_plotters_no_img, tmpdir, spatial_kwargs): if "bw" in spatial_kwargs: # Has no meaning when there is no image pytest.skip() @@ -406,9 +386,7 @@ def test_white_background_vs_no_img(adata, tmpdir, spatial_kwargs): # These arguments don't make sense for this check pytest.skip() - white_background = np.ones_like( - adata.uns["spatial"]["scanpy_img"]["images"]["hires"] - ) + white_background = np.ones_like(adata.uns["spatial"]["scanpy_img"]["images"]["hires"]) TESTDIR = Path(tmpdir) white_pth = TESTDIR / "white_background.png" noimg_pth = TESTDIR / "no_img.png" @@ -432,9 +410,7 @@ def test_spatial_na_color(adata, tmpdir): """ Check that na_color defaults to transparent when an image is present, light gray when not. """ - white_background = np.ones_like( - adata.uns["spatial"]["scanpy_img"]["images"]["hires"] - ) + white_background = np.ones_like(adata.uns["spatial"]["scanpy_img"]["images"]["hires"]) TESTDIR = Path(tmpdir) lightgray_pth = TESTDIR / "lightgray.png" transparent_pth = TESTDIR / "transparent.png" diff --git a/scanpy/tests/test_filter_rank_genes_groups.py b/scanpy/tests/test_filter_rank_genes_groups.py index 91aff2d27c..9989a81989 100644 --- a/scanpy/tests/test_filter_rank_genes_groups.py +++ b/scanpy/tests/test_filter_rank_genes_groups.py @@ -47,9 +47,7 @@ def test_filter_rank_genes_groups(): 'max_out_group_fraction': 0.5, } - rank_genes_groups( - adata, 'bulk_labels', reference='Dendritic', method='wilcoxon', n_genes=5 - ) + rank_genes_groups(adata, 'bulk_labels', reference='Dendritic', method='wilcoxon', n_genes=5) filter_rank_genes_groups(**args) assert np.array_equal( diff --git a/scanpy/tests/test_get.py b/scanpy/tests/test_get.py index 7177fee921..1050e4ad55 100644 --- a/scanpy/tests/test_get.py +++ b/scanpy/tests/test_get.py @@ -39,12 +39,8 @@ def adata(): """ return AnnData( X=np.ones((2, 2)), - obs=pd.DataFrame( - {"obs1": [0, 1], "obs2": ["a", "b"]}, index=["cell1", "cell2"] - ), - var=pd.DataFrame( - {"gene_symbols": ["genesymbol1", "genesymbol2"]}, index=["gene1", "gene2"] - ), + obs=pd.DataFrame({"obs1": [0, 1], "obs2": ["a", "b"]}, index=["cell1", "cell2"]), + var=pd.DataFrame({"gene_symbols": ["genesymbol1", "genesymbol2"]}, index=["gene1", "gene2"]), layers={"double": sparse.csr_matrix(np.ones((2, 2)), dtype=int) * 2}, dtype=int, ) @@ -69,9 +65,7 @@ def test_obs_df(adata): dtype='float64', ) pd.testing.assert_frame_equal( - sc.get.obs_df( - adata, keys=["gene2", "obs1"], obsm_keys=[("eye", 0), ("sparse", 1)] - ), + sc.get.obs_df(adata, keys=["gene2", "obs1"], obsm_keys=[("eye", 0), ("sparse", 1)]), pd.DataFrame( {"gene2": [1, 1], "obs1": [0, 1], "eye-0": [1, 0], "sparse-1": [0.0, 1.0]}, index=adata.obs_names, @@ -361,9 +355,7 @@ def test_repeated_cols(dim, transform, func): adata = transform( sc.AnnData( np.ones((5, 10)), - obs=pd.DataFrame( - np.ones((5, 2)), columns=["a_column_name", "a_column_name"] - ), + obs=pd.DataFrame(np.ones((5, 2)), columns=["a_column_name", "a_column_name"]), var=pd.DataFrame(index=[f"gene-{i}" for i in range(10)]), ) ) @@ -380,9 +372,7 @@ def test_repeated_index_vals(dim, transform, func): adata = transform( sc.AnnData( np.ones((5, 10)), - var=pd.DataFrame( - index=["repeated_id"] * 2 + [f"gene-{i}" for i in range(8)] - ), + var=pd.DataFrame(index=["repeated_id"] * 2 + [f"gene-{i}" for i in range(8)]), ) ) diff --git a/scanpy/tests/test_highly_variable_genes.py b/scanpy/tests/test_highly_variable_genes.py index 8b3e4f52c2..587d895781 100644 --- a/scanpy/tests/test_highly_variable_genes.py +++ b/scanpy/tests/test_highly_variable_genes.py @@ -63,13 +63,9 @@ def test_higly_variable_genes_compare_to_seurat(): sc.pp.normalize_per_cell(pbmc, counts_per_cell_after=1e4) sc.pp.log1p(pbmc) - sc.pp.highly_variable_genes( - pbmc, flavor='seurat', min_mean=0.0125, max_mean=3, min_disp=0.5, inplace=True - ) + sc.pp.highly_variable_genes(pbmc, flavor='seurat', min_mean=0.0125, max_mean=3, min_disp=0.5, inplace=True) - np.testing.assert_array_equal( - seurat_hvg_info['highly_variable'], pbmc.var['highly_variable'] - ) + np.testing.assert_array_equal(seurat_hvg_info['highly_variable'], pbmc.var['highly_variable']) # (still) Not equal to tolerance rtol=2e-05, atol=2e-05 # np.testing.assert_allclose(4, 3.9999, rtol=2e-05, atol=2e-05) @@ -94,9 +90,7 @@ def test_higly_variable_genes_compare_to_seurat(): def test_higly_variable_genes_compare_to_seurat_v3(): - seurat_hvg_info = pd.read_csv( - FILE_V3, sep=' ', dtype={"variances_norm": np.float64} - ) + seurat_hvg_info = pd.read_csv(FILE_V3, sep=' ', dtype={"variances_norm": np.float64}) pbmc = sc.datasets.pbmc3k() pbmc.var_names_make_unique() @@ -107,9 +101,7 @@ def test_higly_variable_genes_compare_to_seurat_v3(): sc.pp.highly_variable_genes(pbmc, n_top_genes=1000, flavor='seurat_v3') sc.pp.highly_variable_genes(pbmc_dense, n_top_genes=1000, flavor='seurat_v3') - np.testing.assert_array_equal( - seurat_hvg_info['highly_variable'], pbmc.var['highly_variable'] - ) + np.testing.assert_array_equal(seurat_hvg_info['highly_variable'], pbmc.var['highly_variable']) np.testing.assert_allclose( seurat_hvg_info['variances'], pbmc.var['variances'], @@ -132,9 +124,7 @@ def test_higly_variable_genes_compare_to_seurat_v3(): batch = np.zeros((len(pbmc)), dtype=int) batch[1500:] = 1 pbmc.obs["batch"] = batch - df = sc.pp.highly_variable_genes( - pbmc, n_top_genes=4000, flavor='seurat_v3', batch_key="batch", inplace=False - ) + df = sc.pp.highly_variable_genes(pbmc, n_top_genes=4000, flavor='seurat_v3', batch_key="batch", inplace=False) df.sort_values( ["highly_variable_nbatches", "highly_variable_rank"], ascending=[False, True], @@ -142,9 +132,7 @@ def test_higly_variable_genes_compare_to_seurat_v3(): inplace=True, ) df = df.iloc[:4000] - seurat_hvg_info_batch = pd.read_csv( - FILE_V3_BATCH, sep=' ', dtype={"variances_norm": np.float64} - ) + seurat_hvg_info_batch = pd.read_csv(FILE_V3_BATCH, sep=' ', dtype={"variances_norm": np.float64}) # ranks might be slightly different due to many genes having same normalized var seu = pd.Index(seurat_hvg_info_batch['x'].values) @@ -176,9 +164,7 @@ def test_filter_genes_dispersion_compare_to_seurat(): min_disp=0.5, ) - np.testing.assert_array_equal( - seurat_hvg_info['highly_variable'], pbmc.var['highly_variable'] - ) + np.testing.assert_array_equal(seurat_hvg_info['highly_variable'], pbmc.var['highly_variable']) # (still) Not equal to tolerance rtol=2e-05, atol=2e-05: # np.testing.assert_allclose(4, 3.9999, rtol=2e-05, atol=2e-05) @@ -219,12 +205,8 @@ def test_highly_variable_genes_batches(): sc.pp.filter_genes(adata_1, min_cells=1) sc.pp.filter_genes(adata_2, min_cells=1) - hvg1 = sc.pp.highly_variable_genes( - adata_1, flavor='cell_ranger', n_top_genes=200, inplace=False - ) - hvg2 = sc.pp.highly_variable_genes( - adata_2, flavor='cell_ranger', n_top_genes=200, inplace=False - ) + hvg1 = sc.pp.highly_variable_genes(adata_1, flavor='cell_ranger', n_top_genes=200, inplace=False) + hvg2 = sc.pp.highly_variable_genes(adata_2, flavor='cell_ranger', n_top_genes=200, inplace=False) assert np.isclose( adata.var['dispersions_norm'][100], @@ -234,9 +216,7 @@ def test_highly_variable_genes_batches(): adata.var['dispersions_norm'][101], 0.5 * hvg1['dispersions_norm'][1] + 0.5 * hvg2['dispersions_norm'][101], ) - assert np.isclose( - adata.var['dispersions_norm'][0], 0.5 * hvg2['dispersions_norm'][0] - ) + assert np.isclose(adata.var['dispersions_norm'][0], 0.5 * hvg2['dispersions_norm'][0]) colnames = [ 'means', diff --git a/scanpy/tests/test_ingest.py b/scanpy/tests/test_ingest.py index a7ba765f98..bf3ad73605 100644 --- a/scanpy/tests/test_ingest.py +++ b/scanpy/tests/test_ingest.py @@ -137,9 +137,7 @@ def test_ingest_map_embedding_umap(): adata_ref = sc.AnnData(X) adata_new = sc.AnnData(T) - sc.pp.neighbors( - adata_ref, method='umap', use_rep='X', n_neighbors=4, random_state=0 - ) + sc.pp.neighbors(adata_ref, method='umap', use_rep='X', n_neighbors=4, random_state=0) sc.tl.umap(adata_ref, random_state=0) ing = sc.tl.Ingest(adata_ref) diff --git a/scanpy/tests/test_neighbors.py b/scanpy/tests/test_neighbors.py index 72df4fbf1a..468b02fdd4 100644 --- a/scanpy/tests/test_neighbors.py +++ b/scanpy/tests/test_neighbors.py @@ -143,13 +143,9 @@ def test_gauss_connectivities_euclidean(neigh): def test_metrics_argument(): no_knn_euclidean = get_neighbors() - no_knn_euclidean.compute_neighbors( - method="gauss", knn=False, n_neighbors=n_neighbors, metric="euclidean" - ) + no_knn_euclidean.compute_neighbors(method="gauss", knn=False, n_neighbors=n_neighbors, metric="euclidean") no_knn_manhattan = get_neighbors() - no_knn_manhattan.compute_neighbors( - method="gauss", knn=False, n_neighbors=n_neighbors, metric="manhattan" - ) + no_knn_manhattan.compute_neighbors(method="gauss", knn=False, n_neighbors=n_neighbors, metric="manhattan") assert not np.allclose(no_knn_euclidean.distances, no_knn_manhattan.distances) diff --git a/scanpy/tests/test_neighbors_key_added.py b/scanpy/tests/test_neighbors_key_added.py index 6793a40d15..db786e170c 100644 --- a/scanpy/tests/test_neighbors_key_added.py +++ b/scanpy/tests/test_neighbors_key_added.py @@ -19,12 +19,8 @@ def test_neighbors_key_added(adata): dists_key = adata.uns[key]['distances_key'] assert adata.uns['neighbors']['params'] == adata.uns[key]['params'] - assert np.allclose( - adata.obsp['connectivities'].toarray(), adata.obsp[conns_key].toarray() - ) - assert np.allclose( - adata.obsp['distances'].toarray(), adata.obsp[dists_key].toarray() - ) + assert np.allclose(adata.obsp['connectivities'].toarray(), adata.obsp[conns_key].toarray()) + assert np.allclose(adata.obsp['distances'].toarray(), adata.obsp[dists_key].toarray()) # test functions with neighbors_key and obsp diff --git a/scanpy/tests/test_package_structure.py b/scanpy/tests/test_package_structure.py index abaa8fc312..bf30f7b03d 100644 --- a/scanpy/tests/test_package_structure.py +++ b/scanpy/tests/test_package_structure.py @@ -15,9 +15,7 @@ proj_dir = mod_dir.parent scanpy_functions = [ - c_or_f - for c_or_f in descend_classes_and_funcs(scanpy, "scanpy") - if isinstance(c_or_f, FunctionType) + c_or_f for c_or_f in descend_classes_and_funcs(scanpy, "scanpy") if isinstance(c_or_f, FunctionType) ] diff --git a/scanpy/tests/test_pca.py b/scanpy/tests/test_pca.py index 975f7ee34d..ad7e4158f4 100644 --- a/scanpy/tests/test_pca.py +++ b/scanpy/tests/test_pca.py @@ -92,9 +92,7 @@ def test_pca_sparse(pbmc3k_normalized): explicit = sc.pp.pca(pbmc_dense, dtype=np.float64, copy=True) assert np.allclose(implicit.uns["pca"]["variance"], explicit.uns["pca"]["variance"]) - assert np.allclose( - implicit.uns["pca"]["variance_ratio"], explicit.uns["pca"]["variance_ratio"] - ) + assert np.allclose(implicit.uns["pca"]["variance_ratio"], explicit.uns["pca"]["variance_ratio"]) assert np.allclose(implicit.obsm['X_pca'], explicit.obsm['X_pca']) assert np.allclose(implicit.varm['PCs'], explicit.varm['PCs']) @@ -125,13 +123,9 @@ def test_pca_chunked(pbmc3k_normalized): default = sc.pp.pca(pbmc3k_normalized, copy=True) # Taking absolute value since sometimes dimensions are flipped - np.testing.assert_allclose( - np.abs(chunked.obsm["X_pca"]), np.abs(default.obsm["X_pca"]) - ) + np.testing.assert_allclose(np.abs(chunked.obsm["X_pca"]), np.abs(default.obsm["X_pca"])) np.testing.assert_allclose(np.abs(chunked.varm["PCs"]), np.abs(default.varm["PCs"])) - np.testing.assert_allclose( - np.abs(chunked.uns["pca"]["variance"]), np.abs(default.uns["pca"]["variance"]) - ) + np.testing.assert_allclose(np.abs(chunked.uns["pca"]["variance"]), np.abs(default.uns["pca"]["variance"])) np.testing.assert_allclose( np.abs(chunked.uns["pca"]["variance_ratio"]), np.abs(default.uns["pca"]["variance_ratio"]), diff --git a/scanpy/tests/test_plotting.py b/scanpy/tests/test_plotting.py index b83d40caa3..8975896b50 100644 --- a/scanpy/tests/test_plotting.py +++ b/scanpy/tests/test_plotting.py @@ -1,3 +1,11 @@ +import scanpy as sc +from anndata import AnnData +from matplotlib.testing.compare import compare_images +import pandas as pd +import numpy as np +import matplotlib.cm as cm +import matplotlib.pyplot as plt +import matplotlib as mpl from functools import partial from pathlib import Path from itertools import repeat, chain, combinations @@ -10,15 +18,6 @@ setup() -import matplotlib as mpl -import matplotlib.pyplot as plt -import matplotlib.cm as cm -import numpy as np -import pandas as pd -from matplotlib.testing.compare import compare_images -from anndata import AnnData - -import scanpy as sc HERE: Path = Path(__file__).parent ROOT = HERE / '_images' @@ -134,14 +133,10 @@ def test_heatmap(image_comparer): var=pd.DataFrame({"genes": 'g1 g2 g3'.split()}).set_index('genes'), ) a.obs['foo'] = a.obs['foo'].astype('category') - sc.pl.heatmap( - a, var_names=a.var_names, groupby='foo', swap_axes=True, figsize=(4, 4) - ) + sc.pl.heatmap(a, var_names=a.var_names, groupby='foo', swap_axes=True, figsize=(4, 4)) save_and_compare_images('master_heatmap_small_swap_alignment') - sc.pl.heatmap( - a, var_names=a.var_names, groupby='foo', swap_axes=False, figsize=(4, 4) - ) + sc.pl.heatmap(a, var_names=a.var_names, groupby='foo', swap_axes=False, figsize=(4, 4)) save_and_compare_images('master_heatmap_small_alignment') @@ -165,9 +160,7 @@ def test_clustermap(image_comparer, obs_keys, name): [ ( "dotplot", - partial( - sc.pl.dotplot, groupby='cell_type', title='dotplot', dendrogram=True - ), + partial(sc.pl.dotplot, groupby='cell_type', title='dotplot', dendrogram=True), ), ( "dotplot2", @@ -421,9 +414,7 @@ def test_tracksplot(image_comparer): save_and_compare_images = image_comparer(ROOT, FIGS, tol=15) adata = sc.datasets.krumsiek11() - sc.pl.tracksplot( - adata, adata.var_names, 'cell_type', dendrogram=True, use_raw=False - ) + sc.pl.tracksplot(adata, adata.var_names, 'cell_type', dendrogram=True, use_raw=False) save_and_compare_images('master_tracksplot') @@ -437,10 +428,15 @@ def test_multiple_plots(image_comparer): 'B-cell': ['CD79A', 'CD79B', 'MS4A1'], 'myeloid': ['CST3', 'LYZ'], } +<<<<<<< HEAD fig, (ax1, ax2, ax3) = plt.subplots( 1, 3, figsize=(20, 5), gridspec_kw={'wspace': 0.7} ) _ = sc.pl.stacked_violin( +======= + fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(20, 5), gridspec_kw={'wspace': 0.7}) + __ = sc.pl.stacked_violin( +>>>>>>> 7a096bf9 (add flake8 pre-commit) adata, markers, groupby='bulk_labels', @@ -560,9 +556,7 @@ def test_correlation(image_comparer): [ ( "ranked_genes_sharey", - partial( - sc.pl.rank_genes_groups, n_genes=12, n_panels_per_row=3, show=False - ), + partial(sc.pl.rank_genes_groups, n_genes=12, n_panels_per_row=3, show=False), ), ( "ranked_genes", @@ -576,9 +570,7 @@ def test_correlation(image_comparer): ), ( "ranked_genes_heatmap", - partial( - sc.pl.rank_genes_groups_heatmap, n_genes=4, cmap='YlGnBu', show=False - ), + partial(sc.pl.rank_genes_groups_heatmap, n_genes=4, cmap='YlGnBu', show=False), ), ( "ranked_genes_heatmap_swap_axes", @@ -814,9 +806,7 @@ def pbmc_scatterplots(): pytest.param( 'tsne', partial(sc.pl.tsne, color=['CD3D', 'louvain']), - marks=pytest.mark.xfail( - reason='slight differences even after setting random_state.' - ), + marks=pytest.mark.xfail(reason='slight differences even after setting random_state.'), ), ('umap_nocolor', sc.pl.umap), ( @@ -1063,10 +1053,7 @@ def test_scatter_rep(tmpdir): ), columns=["rep", "gene", "result"], ) - states["outpth"] = [ - TESTDIR / f"{state.gene}_{state.rep}_{state.result}.png" - for state in states.itertuples() - ] + states["outpth"] = [TESTDIR / f"{state.gene}_{state.rep}_{state.result}.png" for state in states.itertuples()] pattern = np.array(list(chain.from_iterable(repeat(i, 5) for i in range(3)))) coords = np.c_[np.arange(15) % 5, pattern] @@ -1129,10 +1116,7 @@ def test_paga(image_comparer): sc.pl.paga_compare(pbmc, basis='X_pca', legend_fontweight='normal', **common) save_and_compare_images('master_paga_compare_pca') - colors = { - c: {cm.Set1(_): 0.33 for _ in range(3)} - for c in pbmc.obs["bulk_labels"].cat.categories - } + colors = {c: {cm.Set1(_): 0.33 for _ in range(3)} for c in pbmc.obs["bulk_labels"].cat.categories} colors["Dendritic"] = {cm.Set2(_): 0.25 for _ in range(4)} sc.pl.paga(pbmc, color=colors, colorbar=False) diff --git a/scanpy/tests/test_preprocessing.py b/scanpy/tests/test_preprocessing.py index b9decb0579..c2167c4796 100644 --- a/scanpy/tests/test_preprocessing.py +++ b/scanpy/tests/test_preprocessing.py @@ -39,9 +39,7 @@ def base(request): def test_log1p_rep(count_matrix_format, base, dtype): - X = count_matrix_format( - np.abs(sp.random(100, 200, density=0.3, dtype=dtype)).toarray() - ) + X = count_matrix_format(np.abs(sp.random(100, 200, density=0.3, dtype=dtype)).toarray()) check_rep_mutation(sc.pp.log1p, X, base=base) check_rep_results(sc.pp.log1p, X, base=base) @@ -169,15 +167,11 @@ def test_regress_out_ordinal(): adata.obs['n_counts'] = adata.X.sum(axis=1) # results using only one processor - single = sc.pp.regress_out( - adata, keys=['n_counts', 'percent_mito'], n_jobs=1, copy=True - ) + single = sc.pp.regress_out(adata, keys=['n_counts', 'percent_mito'], n_jobs=1, copy=True) assert adata.X.shape == single.X.shape # results using 8 processors - multi = sc.pp.regress_out( - adata, keys=['n_counts', 'percent_mito'], n_jobs=8, copy=True - ) + multi = sc.pp.regress_out(adata, keys=['n_counts', 'percent_mito'], n_jobs=8, copy=True) np.testing.assert_array_equal(single.X, multi.X) @@ -255,15 +249,11 @@ def test_downsample_counts_per_cell(count_matrix_format, replace, dtype): X = X.astype(dtype) adata = AnnData(X=count_matrix_format(X), dtype=dtype) with pytest.raises(ValueError): - sc.pp.downsample_counts( - adata, counts_per_cell=TARGET, total_counts=TARGET, replace=replace - ) + sc.pp.downsample_counts(adata, counts_per_cell=TARGET, total_counts=TARGET, replace=replace) with pytest.raises(ValueError): sc.pp.downsample_counts(adata, replace=replace) initial_totals = np.ravel(adata.X.sum(axis=1)) - adata = sc.pp.downsample_counts( - adata, counts_per_cell=TARGET, replace=replace, copy=True - ) + adata = sc.pp.downsample_counts(adata, counts_per_cell=TARGET, replace=replace, copy=True) new_totals = np.ravel(adata.X.sum(axis=1)) if sp.issparse(adata.X): assert all(adata.X.toarray()[X == 0] == 0) @@ -271,17 +261,13 @@ def test_downsample_counts_per_cell(count_matrix_format, replace, dtype): assert all(adata.X[X == 0] == 0) assert all(new_totals <= TARGET) assert all(initial_totals >= new_totals) - assert all( - initial_totals[initial_totals <= TARGET] == new_totals[initial_totals <= TARGET] - ) + assert all(initial_totals[initial_totals <= TARGET] == new_totals[initial_totals <= TARGET]) if not replace: assert np.all(X >= adata.X) assert X.dtype == adata.X.dtype -def test_downsample_counts_per_cell_multiple_targets( - count_matrix_format, replace, dtype -): +def test_downsample_counts_per_cell_multiple_targets(count_matrix_format, replace, dtype): TARGETS = np.random.randint(500, 1500, 1000) X = np.random.randint(0, 100, (1000, 100)) * np.random.binomial(1, 0.3, (1000, 100)) X = X.astype(dtype) @@ -289,9 +275,7 @@ def test_downsample_counts_per_cell_multiple_targets( initial_totals = np.ravel(adata.X.sum(axis=1)) with pytest.raises(ValueError): sc.pp.downsample_counts(adata, counts_per_cell=[40, 10], replace=replace) - adata = sc.pp.downsample_counts( - adata, counts_per_cell=TARGETS, replace=replace, copy=True - ) + adata = sc.pp.downsample_counts(adata, counts_per_cell=TARGETS, replace=replace, copy=True) new_totals = np.ravel(adata.X.sum(axis=1)) if sp.issparse(adata.X): assert all(adata.X.toarray()[X == 0] == 0) @@ -299,10 +283,7 @@ def test_downsample_counts_per_cell_multiple_targets( assert all(adata.X[X == 0] == 0) assert all(new_totals <= TARGETS) assert all(initial_totals >= new_totals) - assert all( - initial_totals[initial_totals <= TARGETS] - == new_totals[initial_totals <= TARGETS] - ) + assert all(initial_totals[initial_totals <= TARGETS] == new_totals[initial_totals <= TARGETS]) if not replace: assert np.all(X >= adata.X) assert X.dtype == adata.X.dtype @@ -315,9 +296,7 @@ def test_downsample_total_counts(count_matrix_format, replace, dtype): total = X.sum() target = np.floor_divide(total, 10) initial_totals = np.ravel(adata_orig.X.sum(axis=1)) - adata = sc.pp.downsample_counts( - adata_orig, total_counts=target, replace=replace, copy=True - ) + adata = sc.pp.downsample_counts(adata_orig, total_counts=target, replace=replace, copy=True) new_totals = np.ravel(adata.X.sum(axis=1)) if sp.issparse(adata.X): assert all(adata.X.toarray()[X == 0] == 0) @@ -327,9 +306,7 @@ def test_downsample_total_counts(count_matrix_format, replace, dtype): assert all(initial_totals >= new_totals) if not replace: assert np.all(X >= adata.X) - adata = sc.pp.downsample_counts( - adata_orig, total_counts=total + 10, replace=False, copy=True - ) + adata = sc.pp.downsample_counts(adata_orig, total_counts=total + 10, replace=False, copy=True) assert (adata.X == X).all() assert X.dtype == adata.X.dtype diff --git a/scanpy/tests/test_preprocessing_distributed.py b/scanpy/tests/test_preprocessing_distributed.py index 871656c1ce..a24540a75f 100644 --- a/scanpy/tests/test_preprocessing_distributed.py +++ b/scanpy/tests/test_preprocessing_distributed.py @@ -16,9 +16,7 @@ installed = {mod: bool(find_spec(mod)) for mod in required} -@pytest.mark.skipif( - not all(installed.values()), reason=f'{required} all required: {installed}' -) +@pytest.mark.skipif(not all(installed.values()), reason=f'{required} all required: {installed}') class TestPreprocessingDistributed: @pytest.fixture() def adata(self): diff --git a/scanpy/tests/test_qc_metrics.py b/scanpy/tests/test_qc_metrics.py index 71f6e728e0..33869de5a1 100644 --- a/scanpy/tests/test_qc_metrics.py +++ b/scanpy/tests/test_qc_metrics.py @@ -50,9 +50,7 @@ def test_segments_binary(): assert (segfull == propfull).all() -@pytest.mark.parametrize( - "cls", [np.asarray, sparse.csr_matrix, sparse.csc_matrix, sparse.coo_matrix] -) +@pytest.mark.parametrize("cls", [np.asarray, sparse.csr_matrix, sparse.csc_matrix, sparse.coo_matrix]) def test_top_segments(cls): a = cls(np.ones((300, 100))) seg = top_segment_proportions(a, [50, 100]) @@ -67,25 +65,16 @@ def test_top_segments(cls): # they’re also just making sure the metrics are there def test_qc_metrics(): adata = AnnData(X=sparse.csr_matrix(np.random.binomial(100, 0.005, (1000, 1000)))) - adata.var["mito"] = np.concatenate( - (np.ones(100, dtype=bool), np.zeros(900, dtype=bool)) - ) + adata.var["mito"] = np.concatenate((np.ones(100, dtype=bool), np.zeros(900, dtype=bool))) adata.var["negative"] = False sc.pp.calculate_qc_metrics(adata, qc_vars=["mito", "negative"], inplace=True) assert (adata.obs["n_genes_by_counts"] < adata.shape[1]).all() - assert ( - adata.obs["n_genes_by_counts"] >= adata.obs["log1p_n_genes_by_counts"] - ).all() + assert (adata.obs["n_genes_by_counts"] >= adata.obs["log1p_n_genes_by_counts"]).all() assert (adata.obs["total_counts"] == np.ravel(adata.X.sum(axis=1))).all() assert (adata.obs["total_counts"] >= adata.obs["log1p_total_counts"]).all() - assert ( - adata.obs["total_counts_mito"] >= adata.obs["log1p_total_counts_mito"] - ).all() + assert (adata.obs["total_counts_mito"] >= adata.obs["log1p_total_counts_mito"]).all() assert (adata.obs["total_counts_negative"] == 0).all() - assert ( - adata.obs["pct_counts_in_top_50_genes"] - <= adata.obs["pct_counts_in_top_100_genes"] - ).all() + assert (adata.obs["pct_counts_in_top_50_genes"] <= adata.obs["pct_counts_in_top_100_genes"]).all() for col in filter(lambda x: "negative" not in x, adata.obs.columns): assert (adata.obs[col] >= 0).all() # Values should be positive or zero assert (adata.obs[col] != 0).any().all() # Nothing should be all zeros @@ -108,29 +97,21 @@ def test_qc_metrics(): assert np.allclose(adata.var[col], old_var[col]) # with log1p=False adata = AnnData(X=sparse.csr_matrix(np.random.binomial(100, 0.005, (1000, 1000)))) - adata.var["mito"] = np.concatenate( - (np.ones(100, dtype=bool), np.zeros(900, dtype=bool)) - ) + adata.var["mito"] = np.concatenate((np.ones(100, dtype=bool), np.zeros(900, dtype=bool))) adata.var["negative"] = False - sc.pp.calculate_qc_metrics( - adata, qc_vars=["mito", "negative"], log1p=False, inplace=True - ) + sc.pp.calculate_qc_metrics(adata, qc_vars=["mito", "negative"], log1p=False, inplace=True) assert not np.any(adata.obs.columns.str.startswith("log1p_")) assert not np.any(adata.var.columns.str.startswith("log1p_")) def adata_mito(): a = np.random.binomial(100, 0.005, (1000, 1000)) - init_var = pd.DataFrame( - dict(mito=np.concatenate((np.ones(100, dtype=bool), np.zeros(900, dtype=bool)))) - ) + init_var = pd.DataFrame(dict(mito=np.concatenate((np.ones(100, dtype=bool), np.zeros(900, dtype=bool))))) adata_dense = AnnData(X=a, var=init_var.copy()) return adata_dense, init_var -@pytest.mark.parametrize( - "cls", [np.asarray, sparse.csr_matrix, sparse.csc_matrix, sparse.coo_matrix] -) +@pytest.mark.parametrize("cls", [np.asarray, sparse.csr_matrix, sparse.csc_matrix, sparse.coo_matrix]) def test_qc_metrics_format(cls): adata_dense, init_var = adata_mito() sc.pp.calculate_qc_metrics(adata_dense, qc_vars=["mito"], inplace=True) diff --git a/scanpy/tests/test_queries.py b/scanpy/tests/test_queries.py index e4ef9cc69d..c97e2e6767 100644 --- a/scanpy/tests/test_queries.py +++ b/scanpy/tests/test_queries.py @@ -20,9 +20,7 @@ def test_enrich(): sc.queries.enrich(pbmc, "1") gene_dict = {'set1': ['KLF4', 'PAX5'], 'set2': ['SOX2', 'NANOG']} - enrich_list = sc.queries.enrich( - gene_dict, org="hsapiens", gprofiler_kwargs=dict(sources=['GO:BP']) - ) + enrich_list = sc.queries.enrich(gene_dict, org="hsapiens", gprofiler_kwargs=dict(sources=['GO:BP'])) assert 'set1' in enrich_list['query'].unique() assert 'set2' in enrich_list['query'].unique() @@ -31,6 +29,4 @@ def test_enrich(): def test_mito_genes(): pbmc = sc.datasets.pbmc68k_reduced() mt_genes = sc.queries.mitochondrial_genes("hsapiens") - assert ( - pbmc.var_names.isin(mt_genes["external_gene_name"]).sum() == 1 - ) # Should only be MT-ND3 + assert pbmc.var_names.isin(mt_genes["external_gene_name"]).sum() == 1 # Should only be MT-ND3 diff --git a/scanpy/tests/test_rank_genes_groups.py b/scanpy/tests/test_rank_genes_groups.py index 1592a5c6a8..269adb4990 100644 --- a/scanpy/tests/test_rank_genes_groups.py +++ b/scanpy/tests/test_rank_genes_groups.py @@ -28,13 +28,9 @@ def get_example_data(*, sparse=False): # create test object - adata = AnnData( - np.multiply(binomial(1, 0.15, (100, 20)), negative_binomial(2, 0.25, (100, 20))) - ) + adata = AnnData(np.multiply(binomial(1, 0.15, (100, 20)), negative_binomial(2, 0.25, (100, 20)))) # adapt marker_genes for cluster (so as to have some form of reasonable input - adata.X[0:10, 0:5] = np.multiply( - binomial(1, 0.9, (10, 5)), negative_binomial(1, 0.5, (10, 5)) - ) + adata.X[0:10, 0:5] = np.multiply(binomial(1, 0.9, (10, 5)), negative_binomial(1, 0.5, (10, 5))) # The following construction is inefficient, but makes sure that the same data is used in the sparse case if sparse: @@ -81,30 +77,22 @@ def test_results_dense(): rank_genes_groups(adata, 'true_groups', n_genes=20, method='t-test') - adata.uns['rank_genes_groups']['names'] = adata.uns['rank_genes_groups'][ - 'names' - ].astype(true_names_t_test.dtype) + adata.uns['rank_genes_groups']['names'] = adata.uns['rank_genes_groups']['names'].astype(true_names_t_test.dtype) for name in true_scores_t_test.dtype.names: - assert np.allclose( - true_scores_t_test[name], adata.uns['rank_genes_groups']['scores'][name] - ) + assert np.allclose(true_scores_t_test[name], adata.uns['rank_genes_groups']['scores'][name]) assert np.array_equal(true_names_t_test, adata.uns['rank_genes_groups']['names']) rank_genes_groups(adata, 'true_groups', n_genes=20, method='wilcoxon') - adata.uns['rank_genes_groups']['names'] = adata.uns['rank_genes_groups'][ - 'names' - ].astype(true_names_wilcoxon.dtype) + adata.uns['rank_genes_groups']['names'] = adata.uns['rank_genes_groups']['names'].astype(true_names_wilcoxon.dtype) for name in true_scores_t_test.dtype.names: assert np.allclose( true_scores_wilcoxon[name][:7], adata.uns['rank_genes_groups']['scores'][name][:7], ) - assert np.array_equal( - true_names_wilcoxon[:7], adata.uns['rank_genes_groups']['names'][:7] - ) + assert np.array_equal(true_names_wilcoxon[:7], adata.uns['rank_genes_groups']['names'][:7]) def test_results_sparse(): @@ -121,30 +109,22 @@ def test_results_sparse(): rank_genes_groups(adata, 'true_groups', n_genes=20, method='t-test') - adata.uns['rank_genes_groups']['names'] = adata.uns['rank_genes_groups'][ - 'names' - ].astype(true_names_t_test.dtype) + adata.uns['rank_genes_groups']['names'] = adata.uns['rank_genes_groups']['names'].astype(true_names_t_test.dtype) for name in true_scores_t_test.dtype.names: - assert np.allclose( - true_scores_t_test[name], adata.uns['rank_genes_groups']['scores'][name] - ) + assert np.allclose(true_scores_t_test[name], adata.uns['rank_genes_groups']['scores'][name]) assert np.array_equal(true_names_t_test, adata.uns['rank_genes_groups']['names']) rank_genes_groups(adata, 'true_groups', n_genes=20, method='wilcoxon') - adata.uns['rank_genes_groups']['names'] = adata.uns['rank_genes_groups'][ - 'names' - ].astype(true_names_wilcoxon.dtype) + adata.uns['rank_genes_groups']['names'] = adata.uns['rank_genes_groups']['names'].astype(true_names_wilcoxon.dtype) for name in true_scores_t_test.dtype.names: assert np.allclose( true_scores_wilcoxon[name][:7], adata.uns['rank_genes_groups']['scores'][name][:7], ) - assert np.array_equal( - true_names_wilcoxon[:7], adata.uns['rank_genes_groups']['names'][:7] - ) + assert np.array_equal(true_names_wilcoxon[:7], adata.uns['rank_genes_groups']['names'][:7]) def test_results_layers(): @@ -235,11 +215,7 @@ def test_wilcoxon_symmetry(): rankby_abs=True, ) - stats_mono = ( - rank_genes_groups_df(pbmc, group="CD14+ Monocyte") - .drop(columns="names") - .to_numpy() - ) + stats_mono = rank_genes_groups_df(pbmc, group="CD14+ Monocyte").drop(columns="names").to_numpy() rank_genes_groups( pbmc, @@ -250,9 +226,7 @@ def test_wilcoxon_symmetry(): rankby_abs=True, ) - stats_dend = ( - rank_genes_groups_df(pbmc, group="Dendritic").drop(columns="names").to_numpy() - ) + stats_dend = rank_genes_groups_df(pbmc, group="Dendritic").drop(columns="names").to_numpy() assert np.allclose(np.abs(stats_mono), np.abs(stats_dend)) diff --git a/scanpy/tests/test_rank_genes_groups_logreg.py b/scanpy/tests/test_rank_genes_groups_logreg.py index f64a3c3fb7..a13997458e 100644 --- a/scanpy/tests/test_rank_genes_groups_logreg.py +++ b/scanpy/tests/test_rank_genes_groups_logreg.py @@ -34,9 +34,7 @@ def test_rank_genes_groups_with_renamed_categories_use_rep(): adata.X = adata.X[::-1, :] sc.tl.louvain(adata) - sc.tl.rank_genes_groups( - adata, 'louvain', method='logreg', layer="to_test", use_raw=False - ) + sc.tl.rank_genes_groups(adata, 'louvain', method='logreg', layer="to_test", use_raw=False) assert adata.uns['rank_genes_groups']['names'].dtype.names == ('0', '1', '2') assert adata.uns['rank_genes_groups']['names'][0].tolist() == ('3', '1', '0') diff --git a/scanpy/tests/test_read_10x.py b/scanpy/tests/test_read_10x.py index 6ed125ab4c..0ee8d7806c 100644 --- a/scanpy/tests/test_read_10x.py +++ b/scanpy/tests/test_read_10x.py @@ -67,9 +67,7 @@ def test_read_10x_h5_v1(): ROOT / '1.2.0' / 'filtered_gene_bc_matrices_h5.h5', genome='hg19_chr21', ) - nospec_genome_v1 = sc.read_10x_h5( - ROOT / '1.2.0' / 'filtered_gene_bc_matrices_h5.h5' - ) + nospec_genome_v1 = sc.read_10x_h5(ROOT / '1.2.0' / 'filtered_gene_bc_matrices_h5.h5') assert_anndata_equal(spec_genome_v1, nospec_genome_v1) @@ -113,6 +111,4 @@ def test_read_visium_counts(): def test_10x_h5_gex(): # Tests that gex option doesn't, say, make the function return None h5_pth = ROOT / '3.0.0' / 'filtered_feature_bc_matrix.h5' - assert_anndata_equal( - sc.read_10x_h5(h5_pth, gex_only=True), sc.read_10x_h5(h5_pth, gex_only=False) - ) + assert_anndata_equal(sc.read_10x_h5(h5_pth, gex_only=True), sc.read_10x_h5(h5_pth, gex_only=False)) diff --git a/scanpy/tests/test_score_genes.py b/scanpy/tests/test_score_genes.py index 9500031f66..497a72e0fc 100644 --- a/scanpy/tests/test_score_genes.py +++ b/scanpy/tests/test_score_genes.py @@ -9,12 +9,7 @@ def _create_random_gene_names(n_genes, name_length): """ creates a bunch of random gene names (just CAPS letters) """ - return np.array( - [ - ''.join(map(chr, np.random.randint(65, 90, name_length))) - for _ in range(n_genes) - ] - ) + return np.array([''.join(map(chr, np.random.randint(65, 90, name_length))) for _ in range(n_genes)]) def _create_sparse_nan_matrix(rows, cols, percent_zero, percent_nan): @@ -57,9 +52,7 @@ def test_add_score(): # the actual genes names are all 6letters # create some non-estinsting names with 7 letters: non_existing_genes = _create_random_gene_names(n_genes=3, name_length=7) - some_genes = np.r_[ - np.unique(np.random.choice(adata.var_names, 10)), np.unique(non_existing_genes) - ] + some_genes = np.r_[np.unique(np.random.choice(adata.var_names, 10)), np.unique(non_existing_genes)] sc.tl.score_genes(adata, some_genes, score_name='Test') assert adata.obs['Test'].dtype == 'float32' @@ -80,12 +73,8 @@ def test_sparse_nanmean(): # sparse matrix with nan S = _create_sparse_nan_matrix(R, C, percent_zero=0.3, percent_nan=0.3) - np.testing.assert_allclose( - np.nanmean(S.A, 1), np.array(_sparse_nanmean(S, 1)).flatten() - ) - np.testing.assert_allclose( - np.nanmean(S.A, 0), np.array(_sparse_nanmean(S, 0)).flatten() - ) + np.testing.assert_allclose(np.nanmean(S.A, 1), np.array(_sparse_nanmean(S, 1)).flatten()) + np.testing.assert_allclose(np.nanmean(S.A, 0), np.array(_sparse_nanmean(S, 0)).flatten()) # edge case of only NaNs per row A = np.full((10, 1), np.nan) @@ -118,9 +107,7 @@ def test_score_genes_sparse_vs_dense(): sc.tl.score_genes(adata_sparse, gene_list=gene_set, score_name='Test') sc.tl.score_genes(adata_dense, gene_list=gene_set, score_name='Test') - np.testing.assert_allclose( - adata_sparse.obs['Test'].values, adata_dense.obs['Test'].values - ) + np.testing.assert_allclose(adata_sparse.obs['Test'].values, adata_dense.obs['Test'].values) def test_score_genes_deplete(): diff --git a/scanpy/tools/_dendrogram.py b/scanpy/tools/_dendrogram.py index 788db127a6..7bcd67328c 100644 --- a/scanpy/tools/_dendrogram.py +++ b/scanpy/tools/_dendrogram.py @@ -109,16 +109,12 @@ def dendrogram( ) if var_names is None: - rep_df = pd.DataFrame( - _choose_representation(adata, use_rep=use_rep, n_pcs=n_pcs) - ) + rep_df = pd.DataFrame(_choose_representation(adata, use_rep=use_rep, n_pcs=n_pcs)) categorical = adata.obs[groupby[0]] if len(groupby) > 1: for group in groupby[1:]: # create new category by merging the given groupby categories - categorical = ( - categorical.astype(str) + "_" + adata.obs[group].astype(str) - ).astype('category') + categorical = (categorical.astype(str) + "_" + adata.obs[group].astype(str)).astype('category') categorical.name = "_".join(groupby) rep_df.set_index(categorical, inplace=True) @@ -137,9 +133,7 @@ def dendrogram( corr_matrix = mean_df.T.corr(method=cor_method) corr_condensed = distance.squareform(1 - corr_matrix) - z_var = sch.linkage( - corr_condensed, method=linkage_method, optimal_ordering=optimal_ordering - ) + z_var = sch.linkage(corr_condensed, method=linkage_method, optimal_ordering=optimal_ordering) dendro_info = sch.dendrogram(z_var, labels=list(categories), no_plot=True) dat = dict( diff --git a/scanpy/tools/_diffmap.py b/scanpy/tools/_diffmap.py index e5b5b8b8f4..a04e3b4a1a 100644 --- a/scanpy/tools/_diffmap.py +++ b/scanpy/tools/_diffmap.py @@ -64,9 +64,7 @@ def diffmap( neighbors_key = 'neighbors' if neighbors_key not in adata.uns: - raise ValueError( - 'You need to run `pp.neighbors` first to compute a neighborhood graph.' - ) + raise ValueError('You need to run `pp.neighbors` first to compute a neighborhood graph.') if n_comps <= 2: raise ValueError('Provide any value greater than 2 for `n_comps`. ') adata = adata.copy() if copy else adata diff --git a/scanpy/tools/_dpt.py b/scanpy/tools/_dpt.py index 6521afecf3..ae191bfa77 100644 --- a/scanpy/tools/_dpt.py +++ b/scanpy/tools/_dpt.py @@ -148,9 +148,7 @@ def dpt( logg.info(' this uses a hierarchical implementation') if dpt.iroot is not None: dpt._set_pseudotime() # pseudotimes are distances from root point - adata.uns[ - 'iroot' - ] = dpt.iroot # update iroot, might have changed when subsampling, for example + adata.uns['iroot'] = dpt.iroot # update iroot, might have changed when subsampling, for example adata.obs['dpt_pseudotime'] = dpt.pseudotime # detect branchings and partition the data into segments if n_branchings > 0: @@ -174,11 +172,7 @@ def dpt( time=start, deep=( 'added\n' - + ( - " 'dpt_pseudotime', the pseudotime (adata.obs)" - if dpt.iroot is not None - else '' - ) + + (" 'dpt_pseudotime', the pseudotime (adata.obs)" if dpt.iroot is not None else '') + ( "\n 'dpt_groups', the branching subgroups of dpt (adata.obs)" "\n 'dpt_order', cell order (adata.obs)" @@ -207,11 +201,7 @@ def __init__( super().__init__(adata, n_dcs=n_dcs, neighbors_key=neighbors_key) self.flavor = 'haghverdi16' self.n_branchings = n_branchings - self.min_group_size = ( - min_group_size - if min_group_size >= 1 - else int(min_group_size * self._adata.shape[0]) - ) + self.min_group_size = min_group_size if min_group_size >= 1 else int(min_group_size * self._adata.shape[0]) self.passed_adata = adata # just for debugging purposes self.choose_largest_segment = False self.allow_kendall_tau_shift = allow_kendall_tau_shift @@ -251,8 +241,7 @@ def detect_branchings(self): List of indices of the tips of segments. """ logg.debug( - f' detect {self.n_branchings} ' - f'branching{"" if self.n_branchings == 1 else "s"}', + f' detect {self.n_branchings} ' f'branching{"" if self.n_branchings == 1 else "s"}', ) # a segment is a subset of points of the data set (defined by the # indices of the points in the segment) @@ -282,11 +271,7 @@ def detect_branchings(self): # # let us define the tips of the whole data set if False: # this is safe, but not compatible with on-the-fly computation - tips_all = np.array( - np.unravel_index( - np.argmax(self.distances_dpt), self.distances_dpt.shape - ) - ) + tips_all = np.array(np.unravel_index(np.argmax(self.distances_dpt), self.distances_dpt.shape)) else: if self.iroot is not None: tip_0 = np.argmax(self.distances_dpt[self.iroot]) @@ -298,10 +283,7 @@ def detect_branchings(self): segs_connects = [[]] segs_undecided = [True] segs_adjacency = [[]] - logg.debug( - ' do not consider groups with less than ' - f'{self.min_group_size} points for splitting' - ) + logg.debug(' do not consider groups with less than ' f'{self.min_group_size} points for splitting') for ibranch in range(self.n_branchings): iseg, tips3 = self.select_segment(segs, segs_tips, segs_undecided) if iseg == -1: @@ -331,9 +313,7 @@ def detect_branchings(self): self.segs_connects[i, seg_adjacency] = segs_connects[i] for i in range(len(segs)): for j in range(len(segs)): - self.segs_adjacency[i, j] = self.distances_dpt[ - self.segs_connects[i, j], self.segs_connects[j, i] - ] + self.segs_adjacency[i, j] = self.distances_dpt[self.segs_connects[i, j], self.segs_connects[j, i]] self.segs_adjacency = self.segs_adjacency.tocsr() self.segs_connects = self.segs_connects.tocsr() @@ -344,13 +324,11 @@ def check_adjacency(self): if n_edges_per_seg[iseg] == n_edges: _ = self.segs_adjacency[iseg].todense().A1 closest_points_other_segs = [ - seg[np.argmin(self.distances_dpt[self.segs_tips[iseg][0], seg])] - for seg in self.segs + seg[np.argmin(self.distances_dpt[self.segs_tips[iseg][0], seg])] for seg in self.segs ] seg = self.segs[iseg] closest_points_in_segs = [ - seg[np.argmin(self.distances_dpt[tips[0], seg])] - for tips in self.segs_tips + seg[np.argmin(self.distances_dpt[tips[0], seg])] for tips in self.segs_tips ] distance_segs = [ self.distances_dpt[closest_points_other_segs[ipoint], point] @@ -403,13 +381,8 @@ def select_segment(self, segs, segs_tips, segs_undecided) -> Tuple[int, int]: # take the inner tip, the "second tip" of the segment for itip in range(2): if ( - self.distances_dpt[ - segs_tips[jseg][1], segs_tips[iseg][itip] - ] - < 0.5 - * self.distances_dpt[ - segs_tips[iseg][~itip], segs_tips[iseg][itip] - ] + self.distances_dpt[segs_tips[jseg][1], segs_tips[iseg][itip]] + < 0.5 * self.distances_dpt[segs_tips[iseg][~itip], segs_tips[iseg][itip]] ): # logg.debug( # ' group', iseg, 'with tip', segs_tips[iseg][itip], @@ -443,9 +416,7 @@ def select_segment(self, segs, segs_tips, segs_undecided) -> Tuple[int, int]: # if we did not normalize, there would be a danger of simply # assigning the highest score to the longest segment score = dseg[tips3[2]] / Dseg[tips3[0], tips3[1]] - score = ( - len(seg) if self.choose_largest_segment else score - ) # simply the number of points + score = len(seg) if self.choose_largest_segment else score # simply the number of points logg.debug( f' group {iseg} score {score} n_points {len(seg)} ' + '(too small)' if len(seg) < self.min_group_size @@ -576,9 +547,7 @@ def detect_branching( segs_tips.insert(iseg, ssegs_tips[trunk]) # append other segments segs += [seg for iseg, seg in enumerate(ssegs) if iseg != trunk] - segs_tips += [ - seg_tips for iseg, seg_tips in enumerate(ssegs_tips) if iseg != trunk - ] + segs_tips += [seg_tips for iseg, seg_tips in enumerate(ssegs_tips) if iseg != trunk] if len(ssegs) == 4: # insert undecided cells at same position segs_undecided.pop(iseg) @@ -610,9 +579,7 @@ def detect_branching( segs_connects[kseg].append(idx) break iseg_cnt += 1 - segs_adjacency[iseg] += list( - range(len(segs_adjacency) - n_add, len(segs_adjacency)) - ) + segs_adjacency[iseg] += list(range(len(segs_adjacency) - n_add, len(segs_adjacency))) segs_connects[iseg] += ssegs_connects[trunk] else: import networkx as nx @@ -628,28 +595,14 @@ def detect_branching( for kseg in kseg_list: reference_point_in_k = segs_tips[kseg][0] closest_points_in_jseg.append( - segs[jseg][ - np.argmin( - self.distances_dpt[reference_point_in_k, segs[jseg]] - ) - ] + segs[jseg][np.argmin(self.distances_dpt[reference_point_in_k, segs[jseg]])] ) # do not use the tip in the large segment j, instead, use the closest point - reference_point_in_j = closest_points_in_jseg[ - -1 - ] # segs_tips[jseg][0] + reference_point_in_j = closest_points_in_jseg[-1] # segs_tips[jseg][0] closest_points_in_kseg.append( - segs[kseg][ - np.argmin( - self.distances_dpt[reference_point_in_j, segs[kseg]] - ) - ] - ) - distances.append( - self.distances_dpt[ - closest_points_in_jseg[-1], closest_points_in_kseg[-1] - ] + segs[kseg][np.argmin(self.distances_dpt[reference_point_in_j, segs[kseg]])] ) + distances.append(self.distances_dpt[closest_points_in_jseg[-1], closest_points_in_kseg[-1]]) # print(jseg, '(', segs_tips[jseg][0], closest_points_in_jseg[-1], ')', # kseg, '(', segs_tips[kseg][0], closest_points_in_kseg[-1], ') :', distances[-1]) idx = np.argmin(distances) @@ -670,42 +623,22 @@ def detect_branching( distances = [] closest_points_in_jseg = [] closest_points_in_kseg = [] - jseg_list = [ - jseg - for jseg in range(len(segs)) - if jseg != kseg and jseg not in prev_connecting_segments - ] + jseg_list = [jseg for jseg in range(len(segs)) if jseg != kseg and jseg not in prev_connecting_segments] for jseg in jseg_list: reference_point_in_k = segs_tips[kseg][0] closest_points_in_jseg.append( - segs[jseg][ - np.argmin( - self.distances_dpt[reference_point_in_k, segs[jseg]] - ) - ] + segs[jseg][np.argmin(self.distances_dpt[reference_point_in_k, segs[jseg]])] ) # do not use the tip in the large segment j, instead, use the closest point - reference_point_in_j = closest_points_in_jseg[ - -1 - ] # segs_tips[jseg][0] + reference_point_in_j = closest_points_in_jseg[-1] # segs_tips[jseg][0] closest_points_in_kseg.append( - segs[kseg][ - np.argmin( - self.distances_dpt[reference_point_in_j, segs[kseg]] - ) - ] - ) - distances.append( - self.distances_dpt[ - closest_points_in_jseg[-1], closest_points_in_kseg[-1] - ] + segs[kseg][np.argmin(self.distances_dpt[reference_point_in_j, segs[kseg]])] ) + distances.append(self.distances_dpt[closest_points_in_jseg[-1], closest_points_in_kseg[-1]]) idx = np.argmin(distances) jseg_min = jseg_list[idx] if jseg_min not in kseg_list: - segs_adjacency_sparse = sp.sparse.lil_matrix( - (len(segs), len(segs)), dtype=float - ) + segs_adjacency_sparse = sp.sparse.lil_matrix((len(segs), len(segs)), dtype=float) for i, seg_adjacency in enumerate(segs_adjacency): segs_adjacency_sparse[i, seg_adjacency] = 1 G = nx.Graph(segs_adjacency_sparse) @@ -719,10 +652,7 @@ def detect_branching( # if we split the cluster, we should not attach kseg do_not_attach_kseg = True else: - logg.debug( - f' cannot attach new segment {kseg} at {jseg_min} ' - '(would produce cycle)' - ) + logg.debug(f' cannot attach new segment {kseg} at {jseg_min} ' '(would produce cycle)') if kseg != kseg_list[-1]: logg.debug(' continue') continue @@ -781,9 +711,7 @@ def _detect_branching( elif self.flavor == 'wolf17_bi' or self.flavor == 'wolf17_bi_un': ssegs = self._detect_branching_single_wolf17_bi(Dseg, tips) else: - raise ValueError( - '`flavor` needs to be in {"haghverdi16", "wolf17_tri", "wolf17_bi"}.' - ) + raise ValueError('`flavor` needs to be in {"haghverdi16", "wolf17_tri", "wolf17_bi"}.') # make sure that each data point has a unique association with a segment masks = np.zeros((len(ssegs), Dseg.shape[0]), dtype=bool) for iseg, seg in enumerate(ssegs): @@ -808,19 +736,13 @@ def _detect_branching( for inewseg, newseg_tips in enumerate(ssegs_tips): reference_point = newseg_tips[0] # closest cell to the new segment within undecided cells - closest_cell = undecided_cells[ - np.argmin(Dseg[reference_point][undecided_cells]) - ] + closest_cell = undecided_cells[np.argmin(Dseg[reference_point][undecided_cells])] ssegs_connects[inewseg].append(closest_cell) # closest cell to the undecided cells within new segment - closest_cell = ssegs[inewseg][ - np.argmin(Dseg[closest_cell][ssegs[inewseg]]) - ] + closest_cell = ssegs[inewseg][np.argmin(Dseg[closest_cell][ssegs[inewseg]])] ssegs_connects[-1].append(closest_cell) # also compute tips for the undecided cells - tip_0 = undecided_cells[ - np.argmax(Dseg[undecided_cells[0]][undecided_cells]) - ] + tip_0 = undecided_cells[np.argmax(Dseg[undecided_cells[0]][undecided_cells])] tip_1 = undecided_cells[np.argmax(Dseg[tip_0][undecided_cells])] ssegs_tips.append([tip_0, tip_1]) ssegs_adjacency = [[3], [3], [3], [0, 1, 2]] @@ -834,59 +756,35 @@ def _detect_branching( # this is another strategy than for the undecided_cells # here it's possible to use the more symmetric procedure # shouldn't make much of a difference - closest_points[0, 1] = ssegs[1][ - np.argmin(Dseg[reference_point[0]][ssegs[1]]) - ] - closest_points[1, 0] = ssegs[0][ - np.argmin(Dseg[reference_point[1]][ssegs[0]]) - ] - closest_points[0, 2] = ssegs[2][ - np.argmin(Dseg[reference_point[0]][ssegs[2]]) - ] - closest_points[2, 0] = ssegs[0][ - np.argmin(Dseg[reference_point[2]][ssegs[0]]) - ] - closest_points[1, 2] = ssegs[2][ - np.argmin(Dseg[reference_point[1]][ssegs[2]]) - ] - closest_points[2, 1] = ssegs[1][ - np.argmin(Dseg[reference_point[2]][ssegs[1]]) - ] + closest_points[0, 1] = ssegs[1][np.argmin(Dseg[reference_point[0]][ssegs[1]])] + closest_points[1, 0] = ssegs[0][np.argmin(Dseg[reference_point[1]][ssegs[0]])] + closest_points[0, 2] = ssegs[2][np.argmin(Dseg[reference_point[0]][ssegs[2]])] + closest_points[2, 0] = ssegs[0][np.argmin(Dseg[reference_point[2]][ssegs[0]])] + closest_points[1, 2] = ssegs[2][np.argmin(Dseg[reference_point[1]][ssegs[2]])] + closest_points[2, 1] = ssegs[1][np.argmin(Dseg[reference_point[2]][ssegs[1]])] added_dist = np.zeros(3) added_dist[0] = ( - Dseg[closest_points[1, 0], closest_points[0, 1]] - + Dseg[closest_points[2, 0], closest_points[0, 2]] + Dseg[closest_points[1, 0], closest_points[0, 1]] + Dseg[closest_points[2, 0], closest_points[0, 2]] ) added_dist[1] = ( - Dseg[closest_points[0, 1], closest_points[1, 0]] - + Dseg[closest_points[2, 1], closest_points[1, 2]] + Dseg[closest_points[0, 1], closest_points[1, 0]] + Dseg[closest_points[2, 1], closest_points[1, 2]] ) added_dist[2] = ( - Dseg[closest_points[1, 2], closest_points[2, 1]] - + Dseg[closest_points[0, 2], closest_points[2, 0]] + Dseg[closest_points[1, 2], closest_points[2, 1]] + Dseg[closest_points[0, 2], closest_points[2, 0]] ) trunk = np.argmin(added_dist) - ssegs_adjacency = [ - [trunk] if i != trunk else [j for j in range(3) if j != trunk] - for i in range(3) - ] + ssegs_adjacency = [[trunk] if i != trunk else [j for j in range(3) if j != trunk] for i in range(3)] ssegs_connects = [ - [closest_points[i, trunk]] - if i != trunk - else [closest_points[trunk, j] for j in range(3) if j != trunk] + [closest_points[i, trunk]] if i != trunk else [closest_points[trunk, j] for j in range(3) if j != trunk] for i in range(3) ] else: trunk = 0 ssegs_adjacency = [[1], [0]] reference_point_in_0 = ssegs_tips[0][0] - closest_point_in_1 = ssegs[1][ - np.argmin(Dseg[reference_point_in_0][ssegs[1]]) - ] + closest_point_in_1 = ssegs[1][np.argmin(Dseg[reference_point_in_0][ssegs[1]])] reference_point_in_1 = closest_point_in_1 # ssegs_tips[1][0] - closest_point_in_0 = ssegs[0][ - np.argmin(Dseg[reference_point_in_1][ssegs[0]]) - ] + closest_point_in_0 = ssegs[0][np.argmin(Dseg[reference_point_in_1][ssegs[0]])] ssegs_connects = [[closest_point_in_1], [closest_point_in_0]] return ssegs, ssegs_tips, ssegs_adjacency, ssegs_connects, trunk @@ -899,9 +797,9 @@ def _detect_branching_single_haghverdi16(self, Dseg, tips): # permutations of tip cells ps = [ [0, 1, 2], # start by computing distances from the first tip - [1, 2, 0], # -"- second tip + [1, 2, 0], # -"- second tip [2, 0, 1], - ] # -"- third tip + ] # -"- third tip for i, p in enumerate(ps): ssegs.append(self.__detect_branching_haghverdi16(Dseg, tips[p])) return ssegs @@ -936,9 +834,7 @@ def _detect_branching_single_wolf17_bi(self, Dseg, tips): ssegs = [closer_to_0_than_to_1, ~closer_to_0_than_to_1] return ssegs - def __detect_branching_haghverdi16( - self, Dseg: np.ndarray, tips: np.ndarray - ) -> np.ndarray: + def __detect_branching_haghverdi16(self, Dseg: np.ndarray, tips: np.ndarray) -> np.ndarray: """\ Detect branching on given segment. @@ -976,9 +872,7 @@ def __detect_branching_haghverdi16( # highly different, one would need to write the following equation # in terms of an ordering, such as exploited by the kendall # correlation method above - imax = np.argmin( - Dseg[tips[0]][idcs] + Dseg[tips[1]][idcs] + Dseg[tips[2]][idcs] - ) + imax = np.argmin(Dseg[tips[0]][idcs] + Dseg[tips[1]][idcs] + Dseg[tips[2]][idcs]) # init list to store new segments ssegs = [] # noqa: F841 # first new segment: all points until, but excluding the branching point diff --git a/scanpy/tools/_draw_graph.py b/scanpy/tools/_draw_graph.py index 0447880388..a8c55f3fa7 100644 --- a/scanpy/tools/_draw_graph.py +++ b/scanpy/tools/_draw_graph.py @@ -156,9 +156,7 @@ def draw_graph( iterations = kwds['iterations'] else: iterations = 500 - positions = forceatlas2.forceatlas2( - adjacency, pos=init_coords, iterations=iterations - ) + positions = forceatlas2.forceatlas2(adjacency, pos=init_coords, iterations=iterations) positions = np.array(positions) else: # igraph doesn't use numpy seed diff --git a/scanpy/tools/_embedding_density.py b/scanpy/tools/_embedding_density.py index b946837f2c..64c9794db6 100644 --- a/scanpy/tools/_embedding_density.py +++ b/scanpy/tools/_embedding_density.py @@ -119,8 +119,7 @@ def embedding_density( if f'X_{basis}' not in adata.obsm_keys(): raise ValueError( - "Cannot find the embedded representation " - f"`adata.obsm['X_{basis}']`. Compute the embedding first." + "Cannot find the embedded representation " f"`adata.obsm['X_{basis}']`. Compute the embedding first." ) if components is None: @@ -181,9 +180,7 @@ def embedding_density( if basis != 'diffmap': components += 1 - adata.uns[f'{density_covariate}_params'] = dict( - covariate=groupby, components=components.tolist() - ) + adata.uns[f'{density_covariate}_params'] = dict(covariate=groupby, components=components.tolist()) logg.hint( f"added\n" diff --git a/scanpy/tools/_ingest.py b/scanpy/tools/_ingest.py index 0941ded814..ed09731c48 100644 --- a/scanpy/tools/_ingest.py +++ b/scanpy/tools/_ingest.py @@ -113,12 +113,8 @@ def ingest( start = logg.info('running ingest') obs = [obs] if isinstance(obs, str) else obs - embedding_method = ( - [embedding_method] if isinstance(embedding_method, str) else embedding_method - ) - labeling_method = ( - [labeling_method] if isinstance(labeling_method, str) else labeling_method - ) + embedding_method = [embedding_method] if isinstance(embedding_method, str) else embedding_method + labeling_method = [labeling_method] if isinstance(labeling_method, str) else labeling_method if len(labeling_method) == 1 and len(obs or []) > 1: labeling_method = labeling_method * len(obs) @@ -251,9 +247,7 @@ def _init_dist_search(self, dist_args): make_initialized_nnd_search, ) - self._random_init, self._tree_init = make_initialisations( - dist_func, dist_args - ) + self._random_init, self._tree_init = make_initialisations(dist_func, dist_args) _initialise_search = partial( initialise_search, init_from_random=self._random_init, @@ -385,8 +379,7 @@ def __init__(self, adata, neighbors_key=None): self._init_neighbors(adata, neighbors_key) else: raise ValueError( - f'There is no neighbors data in `adata.uns["{neighbors_key}"]`.\n' - 'Please run pp.neighbors.' + f'There is no neighbors data in `adata.uns["{neighbors_key}"]`.\n' 'Please run pp.neighbors.' ) if 'X_umap' in adata.obsm: @@ -436,10 +429,7 @@ def fit(self, adata_new): new_var_names = adata_new.var_names.str.upper() if not ref_var_names.equals(new_var_names): - raise ValueError( - 'Variables in the new adata are different ' - 'from variables in the reference adata' - ) + raise ValueError('Variables in the new adata are different ' 'from variables in the reference adata') self._obs = pd.DataFrame(index=adata_new.obs.index) self._obsm = _DimDict(adata_new.n_obs, axis=0) @@ -473,13 +463,9 @@ def neighbors(self, k=None, queue_size=5, epsilon=0.1, random_state=0): else: from umap.utils import deheap_sort - init = self._initialise_search( - self._rp_forest, train, test, int(k * queue_size), rng_state=rng_state - ) + init = self._initialise_search(self._rp_forest, train, test, int(k * queue_size), rng_state=rng_state) - result = self._search( - train, self._search_graph.indptr, self._search_graph.indices, init, test - ) + result = self._search(train, self._search_graph.indptr, self._search_graph.indices, init, test) indices, dists = deheap_sort(result) self._indices, self._distances = indices[:, :k], dists[:, :k] @@ -499,14 +485,10 @@ def map_embedding(self, method): elif method == 'pca': self._obsm['X_pca'] = self._pca() else: - raise NotImplementedError( - 'Ingest supports only umap and pca embeddings for now.' - ) + raise NotImplementedError('Ingest supports only umap and pca embeddings for now.') def _knn_classify(self, labels): - cat_array = self._adata_ref.obs[labels].astype( - 'category' - ) # ensure it's categorical + cat_array = self._adata_ref.obs[labels].astype('category') # ensure it's categorical values = [cat_array[inds].mode()[0] for inds in self._indices] return pd.Categorical(values=values, categories=cat_array.cat.categories) @@ -542,9 +524,7 @@ def to_adata(self, inplace=False): if not inplace: return adata - def to_adata_joint( - self, batch_key='batch', batch_categories=None, index_unique='-' - ): + def to_adata_joint(self, batch_key='batch', batch_categories=None, index_unique='-'): """\ Returns concatenated object. @@ -565,14 +545,10 @@ def to_adata_joint( for key in self._obsm: if key in self._adata_ref.obsm: - adata.obsm[key] = np.vstack( - (self._adata_ref.obsm[key], self._obsm[key]) - ) + adata.obsm[key] = np.vstack((self._adata_ref.obsm[key], self._obsm[key])) if self._use_rep not in ('X_pca', 'X'): - adata.obsm[self._use_rep] = np.vstack( - (self._adata_ref.obsm[self._use_rep], self._obsm['rep']) - ) + adata.obsm[self._use_rep] = np.vstack((self._adata_ref.obsm[self._use_rep], self._obsm['rep'])) if 'X_umap' in self._obsm: adata.uns['umap'] = self._adata_ref.uns['umap'] diff --git a/scanpy/tools/_louvain.py b/scanpy/tools/_louvain.py index 9b09740284..d8597a7588 100644 --- a/scanpy/tools/_louvain.py +++ b/scanpy/tools/_louvain.py @@ -108,9 +108,7 @@ def louvain( partition_kwargs = dict(partition_kwargs) start = logg.info('running Louvain clustering') if (flavor != 'vtraag') and (partition_type is not None): - raise ValueError( - '`partition_type` is only a valid argument ' 'when `flavour` is "vtraag"' - ) + raise ValueError('`partition_type` is only a valid argument ' 'when `flavour` is "vtraag"') adata = adata.copy() if copy else adata if adjacency is None: adjacency = _choose_graph(adata, obsp, neighbors_key) @@ -182,12 +180,7 @@ def louvain( logg.info(' using the "louvain" package of rapids') louvain_parts, _ = cugraph.louvain(g) - groups = ( - louvain_parts.to_pandas() - .sort_values('vertex')[['partition']] - .to_numpy() - .ravel() - ) + groups = louvain_parts.to_pandas().sort_values('vertex')[['partition']].to_numpy().ravel() elif flavor == 'taynaud': # this is deprecated import networkx as nx diff --git a/scanpy/tools/_marker_gene_overlap.py b/scanpy/tools/_marker_gene_overlap.py index d50932216a..31b1d176dd 100644 --- a/scanpy/tools/_marker_gene_overlap.py +++ b/scanpy/tools/_marker_gene_overlap.py @@ -21,10 +21,7 @@ def _calc_overlap_count(markers1: dict, markers2: dict): overlaps = np.zeros((len(markers1), len(markers2))) for j, marker_group in enumerate(markers1): - tmp = [ - len(markers2[i].intersection(markers1[marker_group])) - for i in markers2.keys() - ] + tmp = [len(markers2[i].intersection(markers1[marker_group])) for i in markers2.keys()] overlaps[j, :] = tmp return overlaps @@ -59,8 +56,7 @@ def _calc_jaccard(markers1: dict, markers2: dict): for j, marker_group in enumerate(markers1): tmp = [ - len(markers2[i].intersection(markers1[marker_group])) - / len(markers2[i].union(markers1[marker_group])) + len(markers2[i].intersection(markers1[marker_group])) / len(markers2[i].union(markers1[marker_group])) for i in markers2.keys() ] jacc_results[j, :] = tmp @@ -158,15 +154,11 @@ def marker_gene_overlap( # Test user inputs if inplace: raise NotImplementedError( - 'Writing Pandas dataframes to h5ad is currently under development.' - '\nPlease use `inplace=False`.' + 'Writing Pandas dataframes to h5ad is currently under development.' '\nPlease use `inplace=False`.' ) if key not in adata.uns: - raise ValueError( - 'Could not find marker gene data. ' - 'Please run `sc.tl.rank_genes_groups()` first.' - ) + raise ValueError('Could not find marker gene data. ' 'Please run `sc.tl.rank_genes_groups()` first.') avail_methods = {'overlap_count', 'overlap_coef', 'jaccard', 'enrich'} if method not in avail_methods: @@ -184,14 +176,9 @@ def marker_gene_overlap( if not all(isinstance(val, cabc.Set) for val in reference_markers.values()): try: - reference_markers = { - key: set(val) for key, val in reference_markers.items() - } + reference_markers = {key: set(val) for key, val in reference_markers.items()} except Exception: - raise ValueError( - 'Please ensure that `reference_markers` contains ' - 'sets or lists of markers as values.' - ) + raise ValueError('Please ensure that `reference_markers` contains ' 'sets or lists of markers as values.') if adj_pval_threshold is not None: if 'pvals_adj' not in adata.uns[key]: @@ -202,26 +189,19 @@ def marker_gene_overlap( ) if adj_pval_threshold < 0: - logg.warning( - '`adj_pval_threshold` was set below 0. Threshold will be set to 0.' - ) + logg.warning('`adj_pval_threshold` was set below 0. Threshold will be set to 0.') adj_pval_threshold = 0 elif adj_pval_threshold > 1: - logg.warning( - '`adj_pval_threshold` was set above 1. Threshold will be set to 1.' - ) + logg.warning('`adj_pval_threshold` was set above 1. Threshold will be set to 1.') adj_pval_threshold = 1 if top_n_markers is not None: logg.warning( - 'Both `adj_pval_threshold` and `top_n_markers` is set. ' - '`adj_pval_threshold` will be ignored.' + 'Both `adj_pval_threshold` and `top_n_markers` is set. ' '`adj_pval_threshold` will be ignored.' ) if top_n_markers is not None and top_n_markers < 1: - logg.warning( - '`top_n_markers` was set below 1. `top_n_markers` will be set to 1.' - ) + logg.warning('`top_n_markers` was set below 1. `top_n_markers` will be set to 1.') top_n_markers = 1 # Get data-derived marker genes in a dictionary of sets @@ -249,16 +229,12 @@ def marker_gene_overlap( marker_match = _calc_overlap_count(reference_markers, data_markers) if normalize == 'reference': # Ensure rows sum to 1 - ref_lengths = np.array( - [len(reference_markers[m_group]) for m_group in reference_markers] - ) + ref_lengths = np.array([len(reference_markers[m_group]) for m_group in reference_markers]) marker_match = marker_match / ref_lengths[:, np.newaxis] marker_match = np.nan_to_num(marker_match) elif normalize == 'data': # Ensure columns sum to 1 - data_lengths = np.array( - [len(data_markers[dat_group]) for dat_group in data_markers] - ) + data_lengths = np.array([len(data_markers[dat_group]) for dat_group in data_markers]) marker_match = marker_match / data_lengths marker_match = np.nan_to_num(marker_match) elif method == 'overlap_coef': @@ -276,9 +252,7 @@ def marker_gene_overlap( # Create a pandas dataframe with the results marker_groups = list(reference_markers.keys()) clusters = list(cluster_ids) - marker_matching_df = pd.DataFrame( - marker_match, index=marker_groups, columns=clusters - ) + marker_matching_df = pd.DataFrame(marker_match, index=marker_groups, columns=clusters) # Store the results if inplace: diff --git a/scanpy/tools/_paga.py b/scanpy/tools/_paga.py index 510f7ce202..1cae9918df 100644 --- a/scanpy/tools/_paga.py +++ b/scanpy/tools/_paga.py @@ -1,4 +1,3 @@ -from collections import namedtuple from typing import List, Optional, NamedTuple import numpy as np @@ -98,9 +97,7 @@ def paga( """ check_neighbors = 'neighbors' if neighbors_key is None else neighbors_key if check_neighbors not in adata.uns: - raise ValueError( - 'You need to run `pp.neighbors` first to compute a neighborhood graph.' - ) + raise ValueError('You need to run `pp.neighbors` first to compute a neighborhood graph.') if groups is None: for k in ("leiden", "louvain"): if k in adata.obs.columns: @@ -161,9 +158,7 @@ def compute_connectivities(self): elif self._model == 'v1.0': return self._compute_connectivities_v1_0() else: - raise ValueError( - f'`model` {self._model} needs to be one of {_AVAIL_MODELS}.' - ) + raise ValueError(f'`model` {self._model} needs to be one of {_AVAIL_MODELS}.') def _compute_connectivities_v1_2(self): import igraph @@ -172,9 +167,7 @@ def _compute_connectivities_v1_2(self): ones.data = np.ones(len(ones.data)) # should be directed if we deal with distances g = _utils.get_igraph_from_adjacency(ones, directed=True) - vc = igraph.VertexClustering( - g, membership=self._adata.obs[self._groups_key].cat.codes.values - ) + vc = igraph.VertexClustering(g, membership=self._adata.obs[self._groups_key].cat.codes.values) ns = vc.sizes() n = sum(ns) es_inner_cluster = [vc.subgraph(i).ecount() for i in range(len(ns))] @@ -208,9 +201,7 @@ def _compute_connectivities_v1_0(self): ones = self._neighbors.connectivities.copy() ones.data = np.ones(len(ones.data)) g = _utils.get_igraph_from_adjacency(ones) - vc = igraph.VertexClustering( - g, membership=self._adata.obs[self._groups_key].cat.codes.values - ) + vc = igraph.VertexClustering(g, membership=self._adata.obs[self._groups_key].cat.codes.values) ns = vc.sizes() cg = vc.cluster_graph(combine_edges='sum') inter_es = _utils.get_sparse_from_igraph(cg, weight_attr='weight') / 2 @@ -235,13 +226,8 @@ def _get_connectivities_tree_v1_2(self): inverse_connectivities = self.connectivities.copy() inverse_connectivities.data = 1.0 / inverse_connectivities.data connectivities_tree = minimum_spanning_tree(inverse_connectivities) - connectivities_tree_indices = [ - connectivities_tree[i].nonzero()[1] - for i in range(connectivities_tree.shape[0]) - ] - connectivities_tree = sp.sparse.lil_matrix( - self.connectivities.shape, dtype=float - ) + connectivities_tree_indices = [connectivities_tree[i].nonzero()[1] for i in range(connectivities_tree.shape[0])] + connectivities_tree = sp.sparse.lil_matrix(self.connectivities.shape, dtype=float) for i, neighbors in enumerate(connectivities_tree_indices): if len(neighbors) > 0: connectivities_tree[i, neighbors] = self.connectivities[i, neighbors] @@ -251,10 +237,7 @@ def _get_connectivities_tree_v1_0(self, inter_es): inverse_inter_es = inter_es.copy() inverse_inter_es.data = 1.0 / inverse_inter_es.data connectivities_tree = minimum_spanning_tree(inverse_inter_es) - connectivities_tree_indices = [ - connectivities_tree[i].nonzero()[1] - for i in range(connectivities_tree.shape[0]) - ] + connectivities_tree_indices = [connectivities_tree[i].nonzero()[1] for i in range(connectivities_tree.shape[0])] connectivities_tree = sp.sparse.lil_matrix(inter_es.shape, dtype=float) for i, neighbors in enumerate(connectivities_tree_indices): if len(neighbors) > 0: @@ -266,9 +249,7 @@ def compute_transitions(self): if vkey not in self._adata.uns: if 'velocyto_transitions' in self._adata.uns: self._adata.uns[vkey] = self._adata.uns['velocyto_transitions'] - logg.debug( - "The key 'velocyto_transitions' has been changed to 'velocity_graph'." - ) + logg.debug("The key 'velocyto_transitions' has been changed to 'velocity_graph'.") else: raise ValueError( 'The passed AnnData needs to have an `uns` annotation ' @@ -289,9 +270,7 @@ def compute_transitions(self): self._adata.uns[vkey].astype('bool'), directed=True, ) - vc = igraph.VertexClustering( - g, membership=self._adata.obs[self._groups_key].cat.codes.values - ) + vc = igraph.VertexClustering(g, membership=self._adata.obs[self._groups_key].cat.codes.values) # set combine_edges to False if you want self loops cg_full = vc.cluster_graph(combine_edges='sum') transitions = _utils.get_sparse_from_igraph(cg_full, weight_attr='weight') @@ -322,9 +301,7 @@ def compute_transitions_old(self): self._adata.uns['velocyto_transitions'], directed=True, ) - vc = igraph.VertexClustering( - g, membership=self._adata.obs[self._groups_key].cat.codes.values - ) + vc = igraph.VertexClustering(g, membership=self._adata.obs[self._groups_key].cat.codes.values) # this stores all single-cell edges in the cluster graph cg_full = vc.cluster_graph(combine_edges=False) # this is the boolean version that simply counts edges in the clustered graph @@ -332,9 +309,7 @@ def compute_transitions_old(self): self._adata.uns['velocyto_transitions'].astype('bool'), directed=True, ) - vc_bool = igraph.VertexClustering( - g_bool, membership=self._adata.obs[self._groups_key].cat.codes.values - ) + vc_bool = igraph.VertexClustering(g_bool, membership=self._adata.obs[self._groups_key].cat.codes.values) cg_bool = vc_bool.cluster_graph(combine_edges='sum') # collapsed version transitions = _utils.get_sparse_from_igraph(cg_bool, weight_attr='weight') total_n = self._neighbors.n_neighbors * np.array(vc_bool.sizes()) @@ -415,16 +390,12 @@ def paga_expression_entropies(adata) -> List[float]: """ from scipy.stats import entropy - groups_order, groups_masks = _utils.select_groups( - adata, key=adata.uns['paga']['groups'] - ) + groups_order, groups_masks = _utils.select_groups(adata, key=adata.uns['paga']['groups']) entropies = [] for mask in groups_masks: X_mask = adata.X[mask].todense() x_median = np.nanmedian(X_mask, axis=1, overwrite_input=True) - x_probs = (x_median - np.nanmin(x_median)) / ( - np.nanmax(x_median) - np.nanmin(x_median) - ) + x_probs = (x_median - np.nanmin(x_median)) / (np.nanmax(x_median) - np.nanmin(x_median)) entropies.append(entropy(x_probs)) return entropies @@ -478,11 +449,7 @@ def paga_compare_paths( import networkx as nx g1 = nx.Graph(adata1.uns['paga'][adjacency_key]) - g2 = nx.Graph( - adata2.uns['paga'][ - adjacency_key2 if adjacency_key2 is not None else adjacency_key - ] - ) + g2 = nx.Graph(adata2.uns['paga'][adjacency_key2 if adjacency_key2 is not None else adjacency_key]) leaf_nodes1 = [str(x) for x in g1.nodes() if g1.degree(x) == 1] logg.debug(f'leaf nodes in graph 1: {leaf_nodes1}') paga_groups = adata1.uns['paga']['groups'] @@ -560,10 +527,7 @@ def paga_compare_paths( if ( ip < ip_progress or l not in p - or not ( - ip + 1 < len(path_mapped) - and path_compare[il + 1] in path_mapped[ip + 1] - ) + or not (ip + 1 < len(path_mapped) and path_compare[il + 1] in path_mapped[ip + 1]) ): continue # make sure that a step backward leads us to the same value of l @@ -581,8 +545,7 @@ def paga_compare_paths( # was ok in the previous step poss = list(range(ip - 1, ip_progress - 2, -1)) logg.debug( - f' step(s) backward to position(s) {poss} ' - 'in path_mapped are fine, too: valid step' + f' step(s) backward to position(s) {poss} ' 'in path_mapped are fine, too: valid step' ) n_agreeing_steps_path += 1 ip_progress = ip + 1 diff --git a/scanpy/tools/_rank_genes_groups.py b/scanpy/tools/_rank_genes_groups.py index 99393f691c..e096f5fd34 100644 --- a/scanpy/tools/_rank_genes_groups.py +++ b/scanpy/tools/_rank_genes_groups.py @@ -104,9 +104,7 @@ def __init__( else: self.expm1_func = np.expm1 - self.groups_order, self.groups_masks = _utils.select_groups( - adata, groups, groupby - ) + self.groups_order, self.groups_masks = _utils.select_groups(adata, groups, groupby) # Singlet groups cause division by zero errors invalid_groups_selected = set(self.groups_order) & set( @@ -171,9 +169,7 @@ def _basic_stats(self): else: mask_rest = self.groups_masks[self.ireference] X_rest = self.X[mask_rest] - self.means[self.ireference], self.vars[self.ireference] = _get_mean_var( - X_rest - ) + self.means[self.ireference], self.vars[self.ireference] = _get_mean_var(X_rest) # deleting the next line causes a memory leak for some reason del X_rest @@ -284,10 +280,7 @@ def wilcoxon(self, tie_correct): m_active = np.count_nonzero(mask_rest) if n_active <= 25 or m_active <= 25: - logg.hint( - 'Few observations in a group for ' - 'normal approximation (<=25). Lower test accuracy.' - ) + logg.hint('Few observations in a group for ' 'normal approximation (<=25). Lower test accuracy.') # Calculate rank sums for each chunk for the current mask for ranks, left, right in _ranks(self.X, mask, mask_rest): @@ -295,13 +288,9 @@ def wilcoxon(self, tie_correct): if tie_correct: T[left:right] = _tiecorrect(ranks) - std_dev = np.sqrt( - T * n_active * m_active * (n_active + m_active + 1) / 12.0 - ) + std_dev = np.sqrt(T * n_active * m_active * (n_active + m_active + 1) / 12.0) - scores = ( - scores - (n_active * ((n_active + m_active + 1) / 2.0)) - ) / std_dev + scores = (scores - (n_active * ((n_active + m_active + 1) / 2.0))) / std_dev scores[np.isnan(scores)] = 0 pvals = 2 * stats.distributions.norm.sf(np.abs(scores)) @@ -331,13 +320,9 @@ def wilcoxon(self, tie_correct): else: T_i = 1 - std_dev = np.sqrt( - T_i * n_active * (n_cells - n_active) * (n_cells + 1) / 12.0 - ) + std_dev = np.sqrt(T_i * n_active * (n_cells - n_active) * (n_cells + 1) / 12.0) - scores[group_index, :] = ( - scores[group_index, :] - (n_active * (n_cells + 1) / 2.0) - ) / std_dev + scores[group_index, :] = (scores[group_index, :] - (n_active * (n_cells + 1) / 2.0)) / std_dev scores[np.isnan(scores)] = 0 pvals = 2 * stats.distributions.norm.sf(np.abs(scores[group_index, :])) @@ -415,9 +400,7 @@ def compute_statistics( from statsmodels.stats.multitest import multipletests pvals[np.isnan(pvals)] = 1 - _, pvals_adj, _, _ = multipletests( - pvals, alpha=0.05, method='fdr_bh' - ) + _, pvals_adj, _, _ = multipletests(pvals, alpha=0.05, method='fdr_bh') elif corr_method == 'bonferroni': pvals_adj = np.minimum(pvals * n_genes, 1.0) self.stats[group_name, 'pvals_adj'] = pvals_adj[global_indices] @@ -431,9 +414,7 @@ def compute_statistics( foldchanges = (self.expm1_func(mean_group) + 1e-9) / ( self.expm1_func(mean_rest) + 1e-9 ) # add small value to remove 0's - self.stats[group_name, 'logfoldchanges'] = np.log2( - foldchanges[global_indices] - ) + self.stats[group_name, 'logfoldchanges'] = np.log2(foldchanges[global_indices]) if n_genes_user is None: self.stats.index = self.var_names @@ -548,9 +529,7 @@ def rank_genes_groups( >>> sc.pl.rank_genes_groups(adata) """ if method is None: - logg.warning( - "Default of the method has been changed to 't-test' from 't-test_overestim_var'" - ) + logg.warning("Default of the method has been changed to 't-test' from 't-test_overestim_var'") method = 't-test' if 'only_positive' in kwds: @@ -580,9 +559,7 @@ def rank_genes_groups( groups_order += [reference] if reference != 'rest' and reference not in adata.obs[groupby].cat.categories: cats = adata.obs[groupby].cat.categories.tolist() - raise ValueError( - f'reference = {reference} needs to be one of groupby = {cats}.' - ) + raise ValueError(f'reference = {reference} needs to be one of groupby = {cats}.') if key_added is None: key_added = 'rank_genes_groups' @@ -608,15 +585,11 @@ def rank_genes_groups( logg.debug(f'consider {groupby!r} groups:') logg.debug(f'with sizes: {np.count_nonzero(test_obj.groups_masks, axis=1)}') - test_obj.compute_statistics( - method, corr_method, n_genes_user, rankby_abs, tie_correct, **kwds - ) + test_obj.compute_statistics(method, corr_method, n_genes_user, rankby_abs, tie_correct, **kwds) if test_obj.pts is not None: groups_names = [str(name) for name in test_obj.groups_order] - adata.uns[key_added]['pts'] = pd.DataFrame( - test_obj.pts.T, index=test_obj.var_names, columns=groups_names - ) + adata.uns[key_added]['pts'] = pd.DataFrame(test_obj.pts.T, index=test_obj.var_names, columns=groups_names) if test_obj.pts_rest is not None: adata.uns[key_added]['pts_rest'] = pd.DataFrame( test_obj.pts_rest.T, index=test_obj.var_names, columns=groups_names @@ -633,9 +606,7 @@ def rank_genes_groups( } for col in test_obj.stats.columns.levels[0]: - adata.uns[key_added][col] = test_obj.stats[col].to_records( - index=False, column_dtypes=dtypes[col] - ) + adata.uns[key_added][col] = test_obj.stats[col].to_records(index=False, column_dtypes=dtypes[col]) logg.info( ' finished', @@ -780,12 +751,8 @@ def expm1_func(x): X_out = sub_X[~in_group] if use_fraction: - fraction_in_cluster_matrix.loc[:, cluster] = ( - adata.uns[key]['pts'][cluster].loc[var_names].values - ) - fraction_out_cluster_matrix.loc[:, cluster] = ( - adata.uns[key]['pts_rest'][cluster].loc[var_names].values - ) + fraction_in_cluster_matrix.loc[:, cluster] = adata.uns[key]['pts'][cluster].loc[var_names].values + fraction_out_cluster_matrix.loc[:, cluster] = adata.uns[key]['pts_rest'][cluster].loc[var_names].values else: fraction_in_cluster_matrix.loc[:, cluster] = _calc_frac(X_in) fraction_out_cluster_matrix.loc[:, cluster] = _calc_frac(X_out) @@ -796,8 +763,7 @@ def expm1_func(x): mean_out_cluster = np.ravel(X_out.mean(0)) # compute fold change fold_change_matrix.loc[:, cluster] = np.log2( - (expm1_func(mean_in_cluster) + 1e-9) - / (expm1_func(mean_out_cluster) + 1e-9) + (expm1_func(mean_in_cluster) + 1e-9) / (expm1_func(mean_out_cluster) + 1e-9) ) # filter original_matrix diff --git a/scanpy/tools/_score_genes.py b/scanpy/tools/_score_genes.py index d336544f0d..a02dfafa12 100644 --- a/scanpy/tools/_score_genes.py +++ b/scanpy/tools/_score_genes.py @@ -32,9 +32,7 @@ def _sparse_nanmean(X, axis): # the average s = Y.sum(axis) - m = s / n_elements.astype( - 'float32' - ) # if we dont cast the int32 to float32, this will result in float64... + m = s / n_elements.astype('float32') # if we dont cast the int32 to float32, this will result in float64... return m @@ -127,22 +125,16 @@ def score_genes( use_raw = _check_use_raw(adata, use_raw) _adata = adata.raw if use_raw else adata - _adata_subset = ( - _adata[:, gene_pool] if len(gene_pool) < len(_adata.var_names) else _adata - ) + _adata_subset = _adata[:, gene_pool] if len(gene_pool) < len(_adata.var_names) else _adata if issparse(_adata_subset.X): obs_avg = pd.Series( np.array(_sparse_nanmean(_adata_subset.X, axis=0)).flatten(), index=gene_pool, ) # average expression of genes else: - obs_avg = pd.Series( - np.nanmean(_adata_subset.X, axis=0), index=gene_pool - ) # average expression of genes + obs_avg = pd.Series(np.nanmean(_adata_subset.X, axis=0), index=gene_pool) # average expression of genes - obs_avg = obs_avg[ - np.isfinite(obs_avg) - ] # Sometimes (and I don't know how) missing data may be there, with nansfor + obs_avg = obs_avg[np.isfinite(obs_avg)] # Sometimes (and I don't know how) missing data may be there, with nansfor n_items = int(np.round(len(obs_avg) / (n_bins - 1))) obs_cut = obs_avg.rank(method='min') // n_items @@ -239,9 +231,7 @@ def score_genes_cell_cycle( adata = adata.copy() if copy else adata ctrl_size = min(len(s_genes), len(g2m_genes)) # add s-score - score_genes( - adata, gene_list=s_genes, score_name='S_score', ctrl_size=ctrl_size, **kwargs - ) + score_genes(adata, gene_list=s_genes, score_name='S_score', ctrl_size=ctrl_size, **kwargs) # add g2m-score score_genes( adata, diff --git a/scanpy/tools/_sim.py b/scanpy/tools/_sim.py index 5bf79f8189..e2e4fcbb68 100644 --- a/scanpy/tools/_sim.py +++ b/scanpy/tools/_sim.py @@ -164,9 +164,7 @@ def sample_dynamic_data(**params): for restart in range(nrRealizations + maxRestarts): # slightly break symmetry in initial conditions if 'toggleswitch' in model_key: - X0 = np.array( - [0.8 for i in range(grnsim.dim)] - ) + 0.01 * np.random.randn(grnsim.dim) + X0 = np.array([0.8 for i in range(grnsim.dim)]) + 0.01 * np.random.randn(grnsim.dim) X = grnsim.sim_model(tmax=tmax, X0=X0, noiseDyn=noiseDyn) # check branching check = True @@ -261,9 +259,7 @@ def sample_dynamic_data(**params): for filename in writedir.glob('sim*.txt'): pass logg.info(f'reading simulation results {filename}') - adata = readwrite._read( - filename, first_column_names=True, suppress_cache_warning=True - ) + adata = readwrite._read(filename, first_column_names=True, suppress_cache_warning=True) adata.uns['tmax_write'] = tmax / step return adata @@ -311,16 +307,12 @@ def write_data( Adj[i, i] = 1 np.savetxt(dir + '/adj_' + id + '.txt', Adj, header=header, fmt='%d') if Coupl.size > 0: - np.savetxt( - dir + '/coupl_' + id + '.txt', Coupl, header=header, fmt='%10.6f' - ) + np.savetxt(dir + '/coupl_' + id + '.txt', Coupl, header=header, fmt='%10.6f') # write model file if varNames and Coupl.size > 0: with (dir / f'model_{id}.txt').open('w') as f: f.write('# For each "variable = ", there must be a right hand side: \n') - f.write( - '# either an empty string or a python-style logical expression \n' - ) + f.write('# either an empty string or a python-style logical expression \n') f.write('# involving variable names, "or", "and", "(", ")". \n') f.write('# The order of equations matters! \n') f.write('# \n') @@ -336,11 +328,7 @@ def write_data( for gp in range(dim): for g in range(dim): if np.abs(Coupl[gp, g]) > 1e-10: - f.write( - f'{names[gp]:10} ' - f'{names[g]:10} ' - f'{Coupl[gp, g]:10.3} \n' - ) + f.write(f'{names[gp]:10} ' f'{names[g]:10} ' f'{Coupl[gp, g]:10.3} \n') # write simulated data # the binary mode option in the following line is a fix for python 3 # variable names @@ -396,9 +384,7 @@ def __init__( either string for predefined model, or directory with a model file and a couple matrix files """ - self.dim = ( - dim if Coupl is None else Coupl.shape[0] - ) # number of nodes / dimension of system + self.dim = dim if Coupl is None else Coupl.shape[0] # number of nodes / dimension of system self.maxnpar = 1 # maximal number of parents self.p_indep = 0.4 # fraction of independent genes self.model = model @@ -475,24 +461,16 @@ def Xdiff_hill(self, Xt): iparent = self.varNames[self.pas[child][iv]] x = Xt[iparent] threshold = 0.1 / np.abs(self.Coupl[ichild, iparent]) - Xdiff_syn_tuple *= ( - self.hill_a(x, threshold) if v else self.hill_i(x, threshold) - ) + Xdiff_syn_tuple *= self.hill_a(x, threshold) if v else self.hill_i(x, threshold) if verbosity > 0: - Xdiff_syn_tuple_str += ( - f'{"a" if v else "i"}' - f'({self.pas[child][iv]}, {threshold:.2})' - ) + Xdiff_syn_tuple_str += f'{"a" if v else "i"}' f'({self.pas[child][iv]}, {threshold:.2})' Xdiff_syn += Xdiff_syn_tuple if verbosity > 0: Xdiff_syn_str += ('+' if ituple != 0 else '') + Xdiff_syn_tuple_str # multiply with degradation term Xdiff[ichild] = self.invTimeStep * (Xdiff_syn - Xt[ichild]) if verbosity > 0: - Xdiff_str = ( - f'{child}_{child}-{child} = ' - f'{self.invTimeStep}*({Xdiff_syn_str}-{child})' - ) + Xdiff_str = f'{child}_{child}-{child} = ' f'{self.invTimeStep}*({Xdiff_syn_str}-{child})' settings.m(0, Xdiff_str) return Xdiff @@ -562,9 +540,7 @@ def read_model(self): # read couplings via names self.Coupl = np.zeros((self.dim, self.dim)) boolContinue = True - for ( - line - ) in self.model.open(): # open(self.model.replace('/model','/couplList')): + for line in self.model.open(): # open(self.model.replace('/model','/couplList')): if line.startswith('# coupling list:'): boolContinue = False if boolContinue: @@ -596,9 +572,7 @@ def set_coupl(self, Coupl=None): for g in range(self.dim): if np.abs(self.Coupl[gp, g] > 1e-10): pas.append(names[g]) - self.boolRules[names[gp]] = ''.join( - pas[:1] + [' or ' + pa for pa in pas[1:]] - ) + self.boolRules[names[gp]] = ''.join(pas[:1] + [' or ' + pa for pa in pas[1:]]) self.Adj_signed = np.sign(Coupl) elif self.model in ['6', '7', '8', '9', '10']: self.Adj_signed = np.zeros((self.dim, self.dim)) @@ -617,14 +591,10 @@ def set_coupl(self, Coupl=None): # settings.m(0,leafnodes,availnodes) while len(availnodes) != 0: # parent - parent_idx = np.random.choice( - np.arange(0, len(leafnodes)), size=1, replace=False - ) + parent_idx = np.random.choice(np.arange(0, len(leafnodes)), size=1, replace=False) parent = leafnodes[parent_idx] # children - children_ids = np.random.choice( - np.arange(0, len(availnodes)), size=2, replace=False - ) + children_ids = np.random.choice(np.arange(0, len(availnodes)), size=2, replace=False) children = availnodes[children_ids] settings.m(0, parent, children) self.Adj_signed[children, parent] = np.ones(2) @@ -651,9 +621,7 @@ def set_coupl(self, Coupl=None): # and the variable itself, therefore its # self.maxnpar+2 in the following line nr = np.random.randint(1, self.maxnpar + 2) - j_par = np.random.choice( - np.arange(0, self.dim), size=nr, replace=False - ) + j_par = np.random.choice(np.arange(0, self.dim), size=nr, replace=False) self.Adj[i, j_par] = 1 else: self.Adj[i, i] = 1 @@ -738,9 +706,7 @@ def sim_model_backwards(self, tmax, X0): X = np.zeros((tmax, self.dim)) X[tmax - 1] = X0 for t in range(tmax - 2, -1, -1): - sol = sp.optimize.root( - self.sim_model_back_help, X[t + 1], args=(X[t + 1]), method='hybr' - ) + sol = sp.optimize.root(self.sim_model_back_help, X[t + 1], args=(X[t + 1]), method='hybr') X[t] = sol.x return X @@ -750,17 +716,12 @@ def branch_init_model1(self, tmax=100): if Xfix[0] > 0.97 or Xfix[0] < 0.03: settings.m( 0, - '... either no fixed point in [0,1]^2! \n' - + ' or fixed point is too close to bounds', + '... either no fixed point in [0,1]^2! \n' + ' or fixed point is too close to bounds', ) return None # - XbackUp = self.sim_model_backwards( - tmax=tmax / 3, X0=Xfix + np.array([0.02, -0.02]) - ) - XbackDo = self.sim_model_backwards( - tmax=tmax / 3, X0=Xfix + np.array([-0.02, -0.02]) - ) + XbackUp = self.sim_model_backwards(tmax=tmax / 3, X0=Xfix + np.array([0.02, -0.02])) + XbackDo = self.sim_model_backwards(tmax=tmax / 3, X0=Xfix + np.array([-0.02, -0.02])) # Xup = self.sim_model(tmax=tmax, X0=XbackUp[0]) Xdo = self.sim_model(tmax=tmax, X0=XbackDo[0]) @@ -783,13 +744,7 @@ def parents_from_boolRule(self, rule): Returns list of parents. """ - rule_pa = ( - rule.replace('(', '') - .replace(')', '') - .replace('or', '') - .replace('and', '') - .replace('not', '') - ) + rule_pa = rule.replace('(', '').replace(')', '').replace('or', '').replace('and', '').replace('not', '') rule_pa = rule_pa.split() # if there are no parents, continue if not rule_pa: @@ -836,17 +791,13 @@ def build_boolCoeff(self): raise ValueError(f'specify coupling value for {key} <- {g}') else: if np.abs(self.Coupl[self.varNames[key], g]) > 1e-10: - raise ValueError( - 'there should be no coupling value for ' f'{key} <- {g}' - ) + raise ValueError('there should be no coupling value for ' f'{key} <- {g}') if self.verbosity > 1: settings.m(0, '...' + key) settings.m(0, rule) settings.m(0, rule_pa) # noqa: F821 # now evaluate coefficients - for tuple in list( - itertools.product([False, True], repeat=len(self.pas[key])) - ): + for tuple in list(itertools.product([False, True], repeat=len(self.pas[key]))): if self.process_rule(rule, self.pas[key], tuple): self.boolCoeff[key].append(tuple) # @@ -974,9 +925,7 @@ def check_nocycles(Adj: np.ndarray, verbosity: int = 2) -> bool: return True -def sample_coupling_matrix( - dim: int = 3, connectivity: float = 0.5 -) -> Tuple[np.ndarray, np.ndarray, np.ndarray, int]: +def sample_coupling_matrix(dim: int = 3, connectivity: float = 0.5) -> Tuple[np.ndarray, np.ndarray, np.ndarray, int]: """\ Sample coupling matrix. @@ -1026,9 +975,7 @@ def sample_coupling_matrix( check = True break if not check: - raise ValueError( - 'did not find graph without cycles after' f'{max_trial} trials' - ) + raise ValueError('did not find graph without cycles after' f'{max_trial} trials') return Coupl, Adj, Adj_signed, n_edges @@ -1220,8 +1167,7 @@ def sample_static_data(model, dir, verbosity=0): # command line options p = argparse.ArgumentParser( description=( - 'Simulate stochastic discrete-time dynamical systems,\n' - 'in particular gene regulatory networks.' + 'Simulate stochastic discrete-time dynamical systems,\n' 'in particular gene regulatory networks.' ), formatter_class=argparse.RawDescriptionHelpFormatter, epilog=( @@ -1260,10 +1206,7 @@ def sample_static_data(model, dir, verbosity=0): model = dir.name.split('_')[0] settings.m(0, f'...model is: {model!r}') if dir.is_dir() and 'test' not in str(dir): - message = ( - f'directory {dir} already exists, ' - 'remove it and continue? [y/n, press enter]' - ) + message = f'directory {dir} already exists, ' 'remove it and continue? [y/n, press enter]' if str(input(message)) != 'y': settings.m(0, ' ...quit program execution') sys.exit() diff --git a/scanpy/tools/_top_genes.py b/scanpy/tools/_top_genes.py index 97f0b7386c..855fef2222 100644 --- a/scanpy/tools/_top_genes.py +++ b/scanpy/tools/_top_genes.py @@ -180,9 +180,7 @@ def ROC_AUC_analysis( fpr[name_list[i]], tpr[name_list[i]], thresholds[name_list[i]], - ) = metrics.roc_curve( - y_true, y_score, pos_label=None, sample_weight=None, drop_intermediate=False - ) + ) = metrics.roc_curve(y_true, y_score, pos_label=None, sample_weight=None, drop_intermediate=False) roc_auc[name_list[i]] = metrics.auc(fpr[name_list[i]], tpr[name_list[i]]) adata.uns['ROCfpr' + groupby + str(group)] = fpr adata.uns['ROCtpr' + groupby + str(group)] = tpr @@ -191,11 +189,11 @@ def ROC_AUC_analysis( def subsampled_estimates(mask, mask_rest=None, precision=0.01, probability=0.99): - ## Simple method that can be called by rank_gene_group. It uses masks that have been passed to the function and - ## calculates how much has to be subsampled in order to reach a certain precision with a certain probability - ## Then it subsamples for mask, mask rest - ## Since convergence speed varies, we take the slower one, i.e. the variance. This might have future speed-up - ## potential + # Simple method that can be called by rank_gene_group. It uses masks that have been passed to the function and + # calculates how much has to be subsampled in order to reach a certain precision with a certain probability + # Then it subsamples for mask, mask rest + # Since convergence speed varies, we take the slower one, i.e. the variance. This might have future speed-up + # potential if mask_rest is None: mask_rest = ~mask # TODO: DO precision calculation for mean variance shared @@ -204,16 +202,16 @@ def subsampled_estimates(mask, mask_rest=None, precision=0.01, probability=0.99) def dominated_ROC_elimination(adata, grouby): - ## This tool has the purpose to take a set of genes (possibly already pre-selected) and analyze AUC. - ## Those and only those are eliminated who are dominated completely - ## TODO: Potentially (But not till tomorrow), this can be adapted to only consider the AUC in the given - ## TODO: optimization frame + # This tool has the purpose to take a set of genes (possibly already pre-selected) and analyze AUC. + # Those and only those are eliminated who are dominated completely + # TODO: Potentially (But not till tomorrow), this can be adapted to only consider the AUC in the given + # TODO: optimization frame pass def _gene_preselection(adata, mask, thresholds): - ## This tool serves to - ## It is not thought to be addressed directly but rather using rank_genes_group or ROC analysis or comparable - ## TODO: Pass back a truncated adata object with only those genes that fullfill thresholding criterias - ## This function should be accessible by both rank_genes_groups and ROC_curve analysis + # This tool serves to + # It is not thought to be addressed directly but rather using rank_genes_group or ROC analysis or comparable + # TODO: Pass back a truncated adata object with only those genes that fullfill thresholding criterias + # This function should be accessible by both rank_genes_groups and ROC_curve analysis pass diff --git a/scanpy/tools/_tsne_fix.py b/scanpy/tools/_tsne_fix.py index d5a7b663b5..8ee49cd98c 100644 --- a/scanpy/tools/_tsne_fix.py +++ b/scanpy/tools/_tsne_fix.py @@ -125,16 +125,12 @@ def _gradient_descent( if verbose >= 2: print( "[t-SNE] Iteration %d: did not make any progress " - "during the last %d episodes. Finished." - % (i + 1, n_iter_without_progress) + "during the last %d episodes. Finished." % (i + 1, n_iter_without_progress) ) break if grad_norm <= min_grad_norm: if verbose >= 2: - print( - "[t-SNE] Iteration %d: gradient norm %f. Finished." - % (i + 1, grad_norm) - ) + print("[t-SNE] Iteration %d: gradient norm %f. Finished." % (i + 1, grad_norm)) break if error_diff <= min_error_diff: if verbose >= 2: diff --git a/scanpy/tools/_umap.py b/scanpy/tools/_umap.py index 28ee17c229..4267fc479e 100644 --- a/scanpy/tools/_umap.py +++ b/scanpy/tools/_umap.py @@ -122,17 +122,13 @@ def umap( neighbors_key = 'neighbors' if neighbors_key not in adata.uns: - raise ValueError( - f'Did not find .uns["{neighbors_key}"]. Run `sc.pp.neighbors` first.' - ) + raise ValueError(f'Did not find .uns["{neighbors_key}"]. Run `sc.pp.neighbors` first.') start = logg.info('computing UMAP') neighbors = NeighborsView(adata, neighbors_key) if 'params' not in neighbors or neighbors['params']['method'] != 'umap': - logg.warning( - f'.obsp["{neighbors["connectivities_key"]}"] have not been computed using umap' - ) + logg.warning(f'.obsp["{neighbors["connectivities_key"]}"] have not been computed using umap') # Compat for umap 0.4 -> 0.5 with warnings.catch_warnings(): @@ -167,9 +163,7 @@ def simplicial_set_embedding(*args, **kwargs): if isinstance(init_pos, str) and init_pos in adata.obsm.keys(): init_coords = adata.obsm[init_pos] elif isinstance(init_pos, str) and init_pos == 'paga': - init_coords = get_init_pos_from_paga( - adata, random_state=random_state, neighbors_key=neighbors_key - ) + init_coords = get_init_pos_from_paga(adata, random_state=random_state, neighbors_key=neighbors_key) else: init_coords = init_pos # Let umap handle it if hasattr(init_coords, "dtype"): @@ -216,9 +210,7 @@ def simplicial_set_embedding(*args, **kwargs): from cuml import UMAP n_neighbors = neighbors['params']['n_neighbors'] - n_epochs = ( - 500 if maxiter is None else maxiter - ) # 0 is not a valid value for rapids, unlike original umap + n_epochs = 500 if maxiter is None else maxiter # 0 is not a valid value for rapids, unlike original umap X_contiguous = np.ascontiguousarray(X, dtype=np.float32) umap = UMAP( n_neighbors=n_neighbors, diff --git a/scanpy/tools/_utils.py b/scanpy/tools/_utils.py index 856a3f1b45..3d257fe1d2 100644 --- a/scanpy/tools/_utils.py +++ b/scanpy/tools/_utils.py @@ -30,9 +30,7 @@ def _choose_representation(adata, use_rep=None, n_pcs=None, silent=False): if adata.n_vars > settings.N_PCS: if 'X_pca' in adata.obsm.keys(): if n_pcs is not None and n_pcs > adata.obsm['X_pca'].shape[1]: - raise ValueError( - '`X_pca` does not have enough PCs. Rerun `sc.pp.pca` with adjusted `n_comps`.' - ) + raise ValueError('`X_pca` does not have enough PCs. Rerun `sc.pp.pca` with adjusted `n_comps`.') X = adata.obsm['X_pca'][:, :n_pcs] logg.info(f' using \'X_pca\' with n_pcs = {X.shape[1]}') else: @@ -54,10 +52,7 @@ def _choose_representation(adata, use_rep=None, n_pcs=None, silent=False): elif use_rep == 'X': X = adata.X else: - raise ValueError( - 'Did not find {} in `.obsm.keys()`. ' - 'You need to compute it first.'.format(use_rep) - ) + raise ValueError('Did not find {} in `.obsm.keys()`. ' 'You need to compute it first.'.format(use_rep)) settings.verbosity = verbosity # resetting verbosity return X @@ -93,9 +88,7 @@ def preprocess_with_pca(adata, n_pcs: Optional[int] = None, random_state=0): return adata.X -def get_init_pos_from_paga( - adata, adjacency=None, random_state=0, neighbors_key=None, obsp=None -): +def get_init_pos_from_paga(adata, adjacency=None, random_state=0, neighbors_key=None, obsp=None): np.random.seed(random_state) if adjacency is None: adjacency = _choose_graph(adata, obsp, neighbors_key) @@ -117,7 +110,5 @@ def get_init_pos_from_paga( else: init_pos[subset] = group_pos else: - raise ValueError( - 'Plot PAGA first, so that adata.uns[\'paga\']' 'with key \'pos\'.' - ) + raise ValueError('Plot PAGA first, so that adata.uns[\'paga\']' 'with key \'pos\'.') return init_pos diff --git a/scanpy/tools/_utils_clustering.py b/scanpy/tools/_utils_clustering.py index f7b331e6ca..b46b9a2387 100644 --- a/scanpy/tools/_utils_clustering.py +++ b/scanpy/tools/_utils_clustering.py @@ -1,6 +1,4 @@ -def rename_groups( - adata, key_added, restrict_key, restrict_categories, restrict_indices, groups -): +def rename_groups(adata, key_added, restrict_key, restrict_categories, restrict_indices, groups): key_added = restrict_key + '_R' if key_added is None else key_added all_groups = adata.obs[restrict_key].astype('U') prefix = '-'.join(restrict_categories) + ',' @@ -11,14 +9,10 @@ def rename_groups( def restrict_adjacency(adata, restrict_key, restrict_categories, adjacency): if not isinstance(restrict_categories[0], str): - raise ValueError( - 'You need to use strings to label categories, ' 'e.g. \'1\' instead of 1.' - ) + raise ValueError('You need to use strings to label categories, ' 'e.g. \'1\' instead of 1.') for c in restrict_categories: if c not in adata.obs[restrict_key].cat.categories: - raise ValueError( - '\'{}\' is not a valid category for \'{}\''.format(c, restrict_key) - ) + raise ValueError('\'{}\' is not a valid category for \'{}\''.format(c, restrict_key)) restrict_indices = adata.obs[restrict_key].isin(restrict_categories).values adjacency = adjacency[restrict_indices, :] adjacency = adjacency[:, restrict_indices] From 5a144a36d393c94ef326b2158bb5a259a38935bf Mon Sep 17 00:00:00 2001 From: Zethson Date: Wed, 24 Feb 2021 13:46:29 +0100 Subject: [PATCH 39/85] add E402 to flake8 ignore Signed-off-by: Zethson --- .flake8 | 6 ----- .pre-commit-config.yaml | 4 ---- scanpy/external/pp/_scvi.py | 12 ---------- scanpy/neighbors/__init__.py | 4 ---- scanpy/plotting/_tools/__init__.py | 4 ---- scanpy/plotting/_utils.py | 4 ---- scanpy/preprocessing/_simple.py | 6 ----- scanpy/tests/conftest.py | 9 ++++---- .../notebooks/test_paga_paul15_subsampled.py | 3 ++- scanpy/tests/notebooks/test_pbmc3k.py | 3 ++- scanpy/tests/test_plotting.py | 23 ++++++++----------- 11 files changed, 19 insertions(+), 59 deletions(-) diff --git a/.flake8 b/.flake8 index c049b35a37..38d7383b35 100644 --- a/.flake8 +++ b/.flake8 @@ -1,12 +1,6 @@ -<<<<<<< HEAD # Can't yet be moved to the pyproject.toml due to https://gitlab.com/pycqa/flake8/-/issues/428#note_251982786 [flake8] max-line-length = 88 # switched off since they conflict with black's standards ignore = F401, W503, E501, E203, E231, W504, E402, E126, E712, E741, E266, E262 -======= -[flake8] exclude = docs, scanpy/tests -max-line-length = 120 -ignore = F401, W503, E501, E203, E231, W504 ->>>>>>> 7a096bf9 (add flake8 pre-commit) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1c19e2cfaf..861b71dbfc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,7 +7,3 @@ repos: rev: 3.8.4 hooks: - id: flake8 -<<<<<<< HEAD -======= - exclude: scanpy/tests/ ->>>>>>> 7a096bf9 (add flake8 pre-commit) diff --git a/scanpy/external/pp/_scvi.py b/scanpy/external/pp/_scvi.py index 724a5634f7..f4f75b0607 100644 --- a/scanpy/external/pp/_scvi.py +++ b/scanpy/external/pp/_scvi.py @@ -117,13 +117,9 @@ def scvi( from scvi.inference import UnsupervisedTrainer from scvi.dataset import AnnDatasetFromAnnData except ImportError: -<<<<<<< HEAD raise ImportError( "Please install scvi package from https://github.com/YosefLab/scVI" ) -======= - raise ImportError("Please install scvi package from https://github.com/YosefLab/scVI") ->>>>>>> 7a096bf9 (add flake8 pre-commit) # check if observations are unnormalized using first 10 # code from: https://github.com/theislab/dca/blob/89eee4ed01dd969b3d46e0c815382806fbfc2526/dca/io.py#L63-L69 @@ -131,13 +127,9 @@ def scvi( X_subset = adata.X[:10] else: X_subset = adata.X -<<<<<<< HEAD norm_error = ( 'Make sure that the dataset (adata.X) contains unnormalized count data.' ) -======= - norm_error = 'Make sure that the dataset (adata.X) contains unnormalized count data.' ->>>>>>> 7a096bf9 (add flake8 pre-commit) if sp.sparse.issparse(X_subset): assert (X_subset.astype(int) != X_subset).nnz == 0, norm_error else: @@ -193,13 +185,9 @@ def scvi( trainer.train(n_epochs=n_epochs, lr=lr) -<<<<<<< HEAD full = trainer.create_posterior( trainer.model, dataset, indices=np.arange(len(dataset)) ) -======= - full = trainer.create_posterior(trainer.model, dataset, indices=np.arange(len(dataset))) ->>>>>>> 7a096bf9 (add flake8 pre-commit) latent, batch_indices, labels = full.sequential().get_latent() if copy: diff --git a/scanpy/neighbors/__init__.py b/scanpy/neighbors/__init__.py index 542eae78c9..3cc0240d8b 100644 --- a/scanpy/neighbors/__init__.py +++ b/scanpy/neighbors/__init__.py @@ -16,13 +16,9 @@ from .. import settings N_DCS = 15 # default number of diffusion components -<<<<<<< HEAD N_PCS = ( settings.N_PCS ) # Backwards compat, constants should be defined in only one place. -======= -N_PCS = settings.N_PCS # Backwards compat, constants should be defined in only one place. ->>>>>>> 7a096bf9 (add flake8 pre-commit) _Method = Literal['umap', 'gauss', 'rapids'] _MetricFn = Callable[[np.ndarray, np.ndarray], float] diff --git a/scanpy/plotting/_tools/__init__.py b/scanpy/plotting/_tools/__init__.py index d1e6f2e63b..67bae71b2c 100644 --- a/scanpy/plotting/_tools/__init__.py +++ b/scanpy/plotting/_tools/__init__.py @@ -62,11 +62,7 @@ def pca_overview(adata: AnnData, **params): show = params['show'] if 'show' in params else None if 'show' in params: del params['show'] -<<<<<<< HEAD - pca(adata, **params, show=False) -======= scatterplots.pca(adata, **params, show=False) # noqa: F821 ->>>>>>> 7a096bf9 (add flake8 pre-commit) pca_loadings(adata, show=False) pca_variance_ratio(adata, show=show) diff --git a/scanpy/plotting/_utils.py b/scanpy/plotting/_utils.py index e4bf1209fd..71ee0a1622 100644 --- a/scanpy/plotting/_utils.py +++ b/scanpy/plotting/_utils.py @@ -906,11 +906,7 @@ def make_pos(pos, node=root, currentLevel=0, parent=None, vert_loc=0): if levels is None: levels = make_levels({}) else: -<<<<<<< HEAD - levels = {l: {TOTAL: levels[l], CURRENT: 0} for l in levels} -======= levels = {level: {TOTAL: levels[level], CURRENT: 0} for level in levels} ->>>>>>> 7a096bf9 (add flake8 pre-commit) vert_gap = height / (max([level for level in levels]) + 1) return make_pos({}) diff --git a/scanpy/preprocessing/_simple.py b/scanpy/preprocessing/_simple.py index 3995aad9d7..195c2b110c 100644 --- a/scanpy/preprocessing/_simple.py +++ b/scanpy/preprocessing/_simple.py @@ -720,13 +720,7 @@ def scale( annotated with `'mean'` and `'std'` in `adata.var`. """ _check_array_function_arguments(layer=layer, obsm=obsm) -<<<<<<< HEAD - return scale_array( - data, zero_center=zero_center, max_value=max_value, copy=copy # noqa: F821 - ) -======= return scale_array(data, zero_center=zero_center, max_value=max_value, copy=copy) # noqa: F821 ->>>>>>> 7a096bf9 (add flake8 pre-commit) @scale.register(np.ndarray) diff --git a/scanpy/tests/conftest.py b/scanpy/tests/conftest.py index 01c87d95c0..60213c236f 100644 --- a/scanpy/tests/conftest.py +++ b/scanpy/tests/conftest.py @@ -1,13 +1,14 @@ -import scanpy -import pytest -from matplotlib.testing.compare import compare_images, make_test_filename -from matplotlib import pyplot import sys from pathlib import Path import matplotlib as mpl mpl.use('agg') +from matplotlib import pyplot +from matplotlib.testing.compare import compare_images, make_test_filename +import pytest + +import scanpy diff --git a/scanpy/tests/notebooks/test_paga_paul15_subsampled.py b/scanpy/tests/notebooks/test_paga_paul15_subsampled.py index 38c3b4f7d3..cbf7c0d252 100644 --- a/scanpy/tests/notebooks/test_paga_paul15_subsampled.py +++ b/scanpy/tests/notebooks/test_paga_paul15_subsampled.py @@ -3,7 +3,6 @@ # # This is the subsampled notebook for testing. -import scanpy as sc from pathlib import Path import numpy as np @@ -11,6 +10,8 @@ setup() +import scanpy as sc + HERE: Path = Path(__file__).parent ROOT = HERE / '_images_paga_paul15_subsampled' diff --git a/scanpy/tests/notebooks/test_pbmc3k.py b/scanpy/tests/notebooks/test_pbmc3k.py index 42bd171986..b7198d972b 100644 --- a/scanpy/tests/notebooks/test_pbmc3k.py +++ b/scanpy/tests/notebooks/test_pbmc3k.py @@ -10,7 +10,6 @@ # ([here](http://cf.10xgenomics.com/samples/cell-exp/1.1.0/pbmc3k/pbmc3k_filtered_gene_bc_matrices.tar.gz) # from this [webpage](https://support.10xgenomics.com/single-cell-gene-expression/datasets/1.1.0/pbmc3k)). -import scanpy as sc from pathlib import Path import numpy as np @@ -20,6 +19,8 @@ setup() +import scanpy as sc + HERE: Path = Path(__file__).parent ROOT = HERE / 'pbmc3k_images' diff --git a/scanpy/tests/test_plotting.py b/scanpy/tests/test_plotting.py index 8975896b50..b8ead2c9fd 100644 --- a/scanpy/tests/test_plotting.py +++ b/scanpy/tests/test_plotting.py @@ -1,11 +1,3 @@ -import scanpy as sc -from anndata import AnnData -from matplotlib.testing.compare import compare_images -import pandas as pd -import numpy as np -import matplotlib.cm as cm -import matplotlib.pyplot as plt -import matplotlib as mpl from functools import partial from pathlib import Path from itertools import repeat, chain, combinations @@ -18,6 +10,16 @@ setup() +import matplotlib as mpl +import matplotlib.pyplot as plt +import matplotlib.cm as cm +import numpy as np +import pandas as pd +from matplotlib.testing.compare import compare_images +from anndata import AnnData + +import scanpy as sc + HERE: Path = Path(__file__).parent ROOT = HERE / '_images' @@ -428,15 +430,10 @@ def test_multiple_plots(image_comparer): 'B-cell': ['CD79A', 'CD79B', 'MS4A1'], 'myeloid': ['CST3', 'LYZ'], } -<<<<<<< HEAD fig, (ax1, ax2, ax3) = plt.subplots( 1, 3, figsize=(20, 5), gridspec_kw={'wspace': 0.7} ) _ = sc.pl.stacked_violin( -======= - fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(20, 5), gridspec_kw={'wspace': 0.7}) - __ = sc.pl.stacked_violin( ->>>>>>> 7a096bf9 (add flake8 pre-commit) adata, markers, groupby='bulk_labels', From 55aee906f4b2430d598328bfdf204169b2f281e6 Mon Sep 17 00:00:00 2001 From: Zethson Date: Wed, 24 Feb 2021 14:07:13 +0100 Subject: [PATCH 40/85] revert neighbors Signed-off-by: Zethson --- .flake8 | 1 - scanpy/neighbors/__init__.py | 19 +++++++++---------- scanpy/tests/test_plotting.py | 1 - 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/.flake8 b/.flake8 index 38d7383b35..636b14206b 100644 --- a/.flake8 +++ b/.flake8 @@ -3,4 +3,3 @@ max-line-length = 88 # switched off since they conflict with black's standards ignore = F401, W503, E501, E203, E231, W504, E402, E126, E712, E741, E266, E262 -exclude = docs, scanpy/tests diff --git a/scanpy/neighbors/__init__.py b/scanpy/neighbors/__init__.py index 3cc0240d8b..dc3dc8c582 100644 --- a/scanpy/neighbors/__init__.py +++ b/scanpy/neighbors/__init__.py @@ -747,7 +747,7 @@ def compute_neighbors( self.knn = knn X = _choose_representation(self._adata, use_rep=use_rep, n_pcs=n_pcs) # neighbor search - use_dense_distances = (metric == 'euclidean' and X.shape[0] < 8192) or not knn + use_dense_distances = (metric == 'euclidean' and X.shape[0] < 8192) or knn == False if use_dense_distances: _distances = pairwise_distances(X, metric=metric, **metric_kwds) knn_indices, knn_distances = _get_indices_distances_from_dense_matrix(_distances, n_neighbors) @@ -832,7 +832,7 @@ def _compute_connectivities_diffmap(self, density_normalize=True): # make the weight matrix sparse if not self.knn: mask = W > 1e-14 - W[not mask] = 0 + W[mask == False] = 0 else: # restrict number of neighbors to ~k # build a symmetric mask @@ -844,7 +844,7 @@ def _compute_connectivities_diffmap(self, density_normalize=True): W[j, i] = W[i, j] mask[j, i] = True # set all entries that are not nearest neighbors to zero - W[not mask] = 0 + W[mask == False] = 0 else: W = Dsq.copy() # need to copy the distance matrix here; what follows is inplace for i in range(len(Dsq.indptr[:-1])): @@ -987,11 +987,10 @@ def _get_dpt_row(self, i): label = self._connected_components[1][i] mask = self._connected_components[1] == label row = sum( - (self.eigen_values[i] / (1 - self.eigen_values[i]) * (self.eigen_basis[i, i] - self.eigen_basis[:, i])) - ** 2 # noqa: E126 + (self.eigen_values[l] / (1 - self.eigen_values[l]) * (self.eigen_basis[i, l] - self.eigen_basis[:, l])) ** 2 # account for float32 precision - for i in range(0, self.eigen_values.size) - if self.eigen_values[i] < 0.9994 + for l in range(0, self.eigen_values.size) + if self.eigen_values[l] < 0.9994 ) # thanks to Marius Lange for pointing Alex to this: # we will likely remove the contributions from the stationary state below when making @@ -999,9 +998,9 @@ def _get_dpt_row(self, i): # they never seem to have deteriorated results, but also other distance measures (see e.g. # PAGA paper) don't have it, which makes sense row += sum( - (self.eigen_basis[i, j] - self.eigen_basis[:, j]) ** 2 - for j in range(0, self.eigen_values.size) - if self.eigen_values[j] >= 0.9994 + (self.eigen_basis[i, l] - self.eigen_basis[:, l]) ** 2 + for l in range(0, self.eigen_values.size) + if self.eigen_values[l] >= 0.9994 ) if mask is not None: row[~mask] = np.inf diff --git a/scanpy/tests/test_plotting.py b/scanpy/tests/test_plotting.py index b8ead2c9fd..a9eb070be1 100644 --- a/scanpy/tests/test_plotting.py +++ b/scanpy/tests/test_plotting.py @@ -20,7 +20,6 @@ import scanpy as sc - HERE: Path = Path(__file__).parent ROOT = HERE / '_images' FIGS = HERE / 'figures' From fc9d2b6ad35dc52c2ca60041f8e5d6c8d1431d9d Mon Sep 17 00:00:00 2001 From: Zethson Date: Thu, 25 Feb 2021 10:44:48 +0100 Subject: [PATCH 41/85] address review Signed-off-by: Zethson --- docs/extensions/function_images.py | 4 +- docs/extensions/github_links.py | 4 +- pyproject.toml | 2 +- scanpy/_settings.py | 25 ++- scanpy/_utils.py | 78 +++++-- scanpy/cli.py | 24 ++- scanpy/datasets/_datasets.py | 20 +- scanpy/datasets/_ebi_expression_atlas.py | 4 +- scanpy/external/exporting.py | 74 +++++-- scanpy/external/pl.py | 8 +- scanpy/external/pp/_hashsolo.py | 134 ++++++++---- scanpy/external/pp/_magic.py | 11 +- scanpy/external/pp/_mnn_correct.py | 11 +- scanpy/external/pp/_scanorama_integrate.py | 4 +- scanpy/external/pp/_scrublet.py | 28 ++- scanpy/external/tl/_palantir.py | 4 +- scanpy/external/tl/_phate.py | 3 +- scanpy/external/tl/_phenograph.py | 11 +- scanpy/external/tl/_trimap.py | 3 +- scanpy/external/tl/_wishbone.py | 22 +- scanpy/get/get.py | 21 +- scanpy/logging.py | 12 +- scanpy/neighbors/__init__.py | 82 ++++++-- scanpy/plotting/_anndata.py | 152 ++++++++++---- scanpy/plotting/_baseplot_class.py | 48 ++++- scanpy/plotting/_dotplot.py | 40 +++- scanpy/plotting/_matrixplot.py | 8 + scanpy/plotting/_preprocessing.py | 4 +- scanpy/plotting/_qc.py | 10 +- scanpy/plotting/_stacked_violin.py | 40 +++- scanpy/plotting/_tools/__init__.py | 44 +++- scanpy/plotting/_tools/paga.py | 138 ++++++++++--- scanpy/plotting/_tools/scatterplots.py | 139 ++++++++++--- scanpy/plotting/_utils.py | 69 +++++-- scanpy/plotting/palettes.py | 4 +- scanpy/preprocessing/_combat.py | 33 ++- scanpy/preprocessing/_deprecated/__init__.py | 10 +- .../_deprecated/highly_variable_genes.py | 22 +- .../preprocessing/_highly_variable_genes.py | 62 ++++-- scanpy/preprocessing/_normalization.py | 8 +- scanpy/preprocessing/_pca.py | 16 +- scanpy/preprocessing/_qc.py | 51 +++-- scanpy/preprocessing/_recipes.py | 16 +- scanpy/preprocessing/_simple.py | 78 +++++-- scanpy/preprocessing/_utils.py | 4 +- scanpy/queries/_queries.py | 15 +- scanpy/readwrite.py | 65 ++++-- scanpy/tests/external/test_hashsolo.py | 4 +- scanpy/tests/external/test_wishbone.py | 4 +- scanpy/tests/helpers.py | 7 +- .../notebooks/test_paga_paul15_subsampled.py | 4 +- scanpy/tests/notebooks/test_pbmc3k.py | 12 +- scanpy/tests/test_combat.py | 4 +- scanpy/tests/test_datasets.py | 13 +- scanpy/tests/test_embedding_plots.py | 48 +++-- scanpy/tests/test_filter_rank_genes_groups.py | 4 +- scanpy/tests/test_get.py | 20 +- scanpy/tests/test_highly_variable_genes.py | 40 +++- scanpy/tests/test_ingest.py | 4 +- scanpy/tests/test_neighbors.py | 8 +- scanpy/tests/test_neighbors_key_added.py | 8 +- scanpy/tests/test_package_structure.py | 4 +- scanpy/tests/test_pca.py | 13 +- scanpy/tests/test_plotting.py | 38 +++- scanpy/tests/test_preprocessing.py | 45 +++- .../tests/test_preprocessing_distributed.py | 4 +- scanpy/tests/test_qc_metrics.py | 37 +++- scanpy/tests/test_queries.py | 8 +- scanpy/tests/test_rank_genes_groups.py | 50 +++-- scanpy/tests/test_rank_genes_groups_logreg.py | 4 +- scanpy/tests/test_read_10x.py | 8 +- scanpy/tests/test_score_genes.py | 23 ++- scanpy/tools/_dendrogram.py | 12 +- scanpy/tools/_diffmap.py | 4 +- scanpy/tools/_dpt.py | 194 ++++++++++++++---- scanpy/tools/_draw_graph.py | 4 +- scanpy/tools/_embedding_density.py | 7 +- scanpy/tools/_ingest.py | 48 +++-- scanpy/tools/_louvain.py | 11 +- scanpy/tools/_marker_gene_overlap.py | 52 +++-- scanpy/tools/_paga.py | 68 ++++-- scanpy/tools/_rank_genes_groups.py | 68 ++++-- scanpy/tools/_score_genes.py | 20 +- scanpy/tools/_sim.py | 107 +++++++--- scanpy/tools/_top_genes.py | 4 +- scanpy/tools/_tsne_fix.py | 8 +- scanpy/tools/_umap.py | 16 +- scanpy/tools/_utils.py | 17 +- scanpy/tools/_utils_clustering.py | 12 +- 89 files changed, 2074 insertions(+), 657 deletions(-) diff --git a/docs/extensions/function_images.py b/docs/extensions/function_images.py index f2a72840f1..42aac73c26 100644 --- a/docs/extensions/function_images.py +++ b/docs/extensions/function_images.py @@ -6,7 +6,9 @@ from sphinx.ext.autodoc import Options -def insert_function_images(app: Sphinx, what: str, name: str, obj: Any, options: Options, lines: List[str]): +def insert_function_images( + app: Sphinx, what: str, name: str, obj: Any, options: Options, lines: List[str] +): path = app.config.api_dir / f'{name}.png' if what != 'function' or not path.is_file(): return diff --git a/docs/extensions/github_links.py b/docs/extensions/github_links.py index f01106fc4b..a2863627c0 100644 --- a/docs/extensions/github_links.py +++ b/docs/extensions/github_links.py @@ -32,7 +32,9 @@ def __call__( def register_links(app: Sphinx, config: Config): - gh_url = 'https://github.com/{github_user}/{github_repo}'.format_map(config.html_context) + gh_url = 'https://github.com/{github_user}/{github_repo}'.format_map( + config.html_context + ) app.add_role('pr', AutoLink('pr', f'{gh_url}/pull/{{}}', 'PR {}')) app.add_role('issue', AutoLink('issue', f'{gh_url}/issues/{{}}', 'issue {}')) app.add_role('noteversion', AutoLink('noteversion', f'{gh_url}/releases/tag/{{}}')) diff --git a/pyproject.toml b/pyproject.toml index d516c123eb..6b805040d8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -128,7 +128,7 @@ source = ['scanpy'] omit = ['*/tests/*'] [tool.black] -line-length = 120 +line-length = 88 target-version = ['py38'] skip-string-normalization = true exclude = ''' diff --git a/scanpy/_settings.py b/scanpy/_settings.py index fdb4ba3c10..e217b36083 100644 --- a/scanpy/_settings.py +++ b/scanpy/_settings.py @@ -53,7 +53,9 @@ def _type_check(var: Any, varname: str, types: Union[type, Tuple[type, ...]]): possible_types_str = types.__name__ else: type_names = [t.__name__ for t in types] - possible_types_str = "{} or {}".format(", ".join(type_names[:-1]), type_names[-1]) + possible_types_str = "{} or {}".format( + ", ".join(type_names[:-1]), type_names[-1] + ) raise TypeError(f"{varname} must be of type {possible_types_str}") @@ -139,7 +141,9 @@ def verbosity(self) -> Verbosity: @verbosity.setter def verbosity(self, verbosity: Union[Verbosity, int, str]): - verbosity_str_options = [v for v in _VERBOSITY_TO_LOGLEVEL if isinstance(v, str)] + verbosity_str_options = [ + v for v in _VERBOSITY_TO_LOGLEVEL if isinstance(v, str) + ] if isinstance(verbosity, Verbosity): self._verbosity = verbosity elif isinstance(verbosity, int): @@ -148,7 +152,8 @@ def verbosity(self, verbosity: Union[Verbosity, int, str]): verbosity = verbosity.lower() if verbosity not in verbosity_str_options: raise ValueError( - f"Cannot set verbosity to {verbosity}. " f"Accepted string values are: {verbosity_str_options}" + f"Cannot set verbosity to {verbosity}. " + f"Accepted string values are: {verbosity_str_options}" ) else: self._verbosity = Verbosity(verbosity_str_options.index(verbosity)) @@ -180,7 +185,10 @@ def file_format_data(self, file_format: str): _type_check(file_format, "file_format_data", str) file_format_options = {"txt", "csv", "h5ad"} if file_format not in file_format_options: - raise ValueError(f"Cannot set file_format_data to {file_format}. " f"Must be one of {file_format_options}") + raise ValueError( + f"Cannot set file_format_data to {file_format}. " + f"Must be one of {file_format_options}" + ) self._file_format_data = file_format @property @@ -285,7 +293,10 @@ def cache_compression(self) -> Optional[str]: @cache_compression.setter def cache_compression(self, cache_compression: Optional[str]): if cache_compression not in {'lzf', 'gzip', None}: - raise ValueError(f"`cache_compression` ({cache_compression}) " "must be in {'lzf', 'gzip', None}") + raise ValueError( + f"`cache_compression` ({cache_compression}) " + "must be in {'lzf', 'gzip', None}" + ) self._cache_compression = cache_compression @property @@ -464,7 +475,9 @@ def _is_run_from_ipython(): def __str__(self) -> str: return '\n'.join( - f'{k} = {v!r}' for k, v in inspect.getmembers(self) if not k.startswith("_") and not k == 'getdoc' + f'{k} = {v!r}' + for k, v in inspect.getmembers(self) + if not k.startswith("_") and not k == 'getdoc' ) diff --git a/scanpy/_utils.py b/scanpy/_utils.py index 68ab0695c5..a2749c3e1b 100644 --- a/scanpy/_utils.py +++ b/scanpy/_utils.py @@ -54,7 +54,9 @@ def check_versions(): # make this a warning, not an error # it might be useful for people to still be able to run it - logg.warning(f'Scanpy {__version__} needs umap ' f'version >=0.3.0, not {umap_version}.') + logg.warning( + f'Scanpy {__version__} needs umap ' f'version >=0.3.0, not {umap_version}.' + ) def getdoc(c_or_f: Union[Callable, type]) -> Optional[str]: @@ -75,7 +77,8 @@ def type_doc(name: str): return cls return '\n'.join( - f'{line} : {type_doc(line)}' if line.strip() in sig.parameters else line for line in doc.split('\n') + f'{line} : {type_doc(line)}' if line.strip() in sig.parameters else line + for line in doc.split('\n') ) @@ -119,7 +122,9 @@ def _one_of_ours(obj, root: str): return ( hasattr(obj, "__name__") and not obj.__name__.split(".")[-1].startswith("_") - and getattr(obj, '__module__', getattr(obj, '__qualname__', obj.__name__)).startswith(root) + and getattr( + obj, '__module__', getattr(obj, '__qualname__', obj.__name__) + ).startswith(root) ) @@ -169,7 +174,9 @@ def _check_array_function_arguments(**kwargs): # TODO: Figure out a better solution for documenting dispatched functions invalid_args = [k for k, v in kwargs.items() if v is not None] if len(invalid_args) > 0: - raise TypeError(f"Arguments {invalid_args} are only valid if an AnnData object is passed.") + raise TypeError( + f"Arguments {invalid_args} are only valid if an AnnData object is passed." + ) def _check_use_raw(adata: AnnData, use_raw: Union[None, bool]) -> bool: @@ -209,7 +216,8 @@ def get_igraph_from_adjacency(adjacency, directed=None): pass if g.vcount() != adjacency.shape[0]: logg.warning( - f'The constructed graph has only {g.vcount()} nodes. ' 'Your adjacency matrix contained redundant nodes.' + f'The constructed graph has only {g.vcount()} nodes. ' + 'Your adjacency matrix contained redundant nodes.' ) return g @@ -276,12 +284,17 @@ def compute_association_matrix_of_groups( reference labels, entries are proportional to degree of association. """ if normalization not in {'prediction', 'reference'}: - raise ValueError('`normalization` needs to be either "prediction" or "reference".') + raise ValueError( + '`normalization` needs to be either "prediction" or "reference".' + ) sanitize_anndata(adata) cats = adata.obs[reference].cat.categories for cat in cats: if cat in settings.categories_to_ignore: - logg.info(f'Ignoring category {cat!r} ' 'as it’s in `settings.categories_to_ignore`.') + logg.info( + f'Ignoring category {cat!r} ' + 'as it’s in `settings.categories_to_ignore`.' + ) asso_names = [] asso_matrix = [] for ipred_group, pred_group in enumerate(adata.obs[prediction].cat.categories): @@ -300,11 +313,15 @@ def compute_association_matrix_of_groups( if normalization == 'prediction': # compute which fraction of the predicted group is contained in # the ref group - ratio_contained = (np.sum(mask_pred_int) - np.sum(mask_ref_or_pred - mask_ref)) / np.sum(mask_pred_int) + ratio_contained = ( + np.sum(mask_pred_int) - np.sum(mask_ref_or_pred - mask_ref) + ) / np.sum(mask_pred_int) else: # compute which fraction of the reference group is contained in # the predicted group - ratio_contained = (np.sum(mask_ref) - np.sum(mask_ref_or_pred - mask_pred_int)) / np.sum(mask_ref) + ratio_contained = ( + np.sum(mask_ref) - np.sum(mask_ref_or_pred - mask_pred_int) + ) / np.sum(mask_ref) asso_matrix[-1] += [ratio_contained] name_list_pred = [ cats[i] if cats[i] not in settings.categories_to_ignore else '' @@ -312,13 +329,18 @@ def compute_association_matrix_of_groups( if asso_matrix[-1][i] > threshold ] asso_names += ['\n'.join(name_list_pred[:max_n_names])] - Result = namedtuple('compute_association_matrix_of_groups', ['asso_names', 'asso_matrix']) + Result = namedtuple( + 'compute_association_matrix_of_groups', ['asso_names', 'asso_matrix'] + ) return Result(asso_names=asso_names, asso_matrix=np.array(asso_matrix)) def get_associated_colors_of_groups(reference_colors, asso_matrix): return [ - {reference_colors[i_ref]: asso_matrix[i_pred, i_ref] for i_ref in range(asso_matrix.shape[1])} + { + reference_colors[i_ref]: asso_matrix[i_pred, i_ref] + for i_ref in range(asso_matrix.shape[1]) + } for i_pred in range(asso_matrix.shape[0]) ] @@ -347,9 +369,16 @@ def identify_groups(ref_labels, pred_labels, return_overlaps=False): associated_predictions = {} associated_overlaps = {} for ref_label in ref_unique: - sub_pred_unique, sub_pred_counts = np.unique(pred_labels[ref_label == ref_labels], return_counts=True) - relative_overlaps_pred = [sub_pred_counts[i] / pred_dict[n] for i, n in enumerate(sub_pred_unique)] - relative_overlaps_ref = [sub_pred_counts[i] / ref_dict[ref_label] for i, n in enumerate(sub_pred_unique)] + sub_pred_unique, sub_pred_counts = np.unique( + pred_labels[ref_label == ref_labels], return_counts=True + ) + relative_overlaps_pred = [ + sub_pred_counts[i] / pred_dict[n] for i, n in enumerate(sub_pred_unique) + ] + relative_overlaps_ref = [ + sub_pred_counts[i] / ref_dict[ref_label] + for i, n in enumerate(sub_pred_unique) + ] relative_overlaps = np.c_[relative_overlaps_pred, relative_overlaps_ref] relative_overlaps_min = np.min(relative_overlaps, axis=1) pred_best_index = np.argsort(relative_overlaps_min)[::-1] @@ -473,7 +502,9 @@ def select_groups(adata, groups_order_subset='all', key='groups'): if key + '_masks' in adata.uns: groups_masks = adata.uns[key + '_masks'] else: - groups_masks = np.zeros((len(adata.obs[key].cat.categories), adata.obs[key].values.size), dtype=bool) + groups_masks = np.zeros( + (len(adata.obs[key].cat.categories), adata.obs[key].values.size), dtype=bool + ) for iname, name in enumerate(adata.obs[key].cat.categories): # if the name is not found, fallback to index retrieval if adata.obs[key].cat.categories[iname] in adata.obs[key].values: @@ -485,7 +516,9 @@ def select_groups(adata, groups_order_subset='all', key='groups'): if groups_order_subset != 'all': groups_ids = [] for name in groups_order_subset: - groups_ids.append(np.where(adata.obs[key].cat.categories.values == name)[0][0]) + groups_ids.append( + np.where(adata.obs[key].cat.categories.values == name)[0][0] + ) if len(groups_ids) == 0: # fallback to index retrieval groups_ids = np.where( @@ -567,7 +600,9 @@ def subsample( return Xsampled, rows -def subsample_n(X: np.ndarray, n: int = 0, seed: int = 0) -> Tuple[np.ndarray, np.ndarray]: +def subsample_n( + X: np.ndarray, n: int = 0, seed: int = 0 +) -> Tuple[np.ndarray, np.ndarray]: """Subsample n samples from rows of array. Parameters @@ -716,12 +751,17 @@ def __contains__(self, key): def _choose_graph(adata, obsp, neighbors_key): """Choose connectivities from neighbbors or another obsp column""" if obsp is not None and neighbors_key is not None: - raise ValueError('You can\'t specify both obsp, neighbors_key. ' 'Please select only one.') + raise ValueError( + 'You can\'t specify both obsp, neighbors_key. ' 'Please select only one.' + ) if obsp is not None: return adata.obsp[obsp] else: neighbors = NeighborsView(adata, neighbors_key) if 'connectivities' not in neighbors: - raise ValueError('You need to run `pp.neighbors` first ' 'to compute a neighborhood graph.') + raise ValueError( + 'You need to run `pp.neighbors` first ' + 'to compute a neighborhood graph.' + ) return neighbors['connectivities'] diff --git a/scanpy/cli.py b/scanpy/cli.py index b7529d52e6..9a90cebd31 100644 --- a/scanpy/cli.py +++ b/scanpy/cli.py @@ -25,7 +25,9 @@ class _DelegatingSubparsersAction(_SubParsersAction): def __init__(self, *args, _command: str, _runargs: Dict[str, Any], **kwargs): super().__init__(*args, **kwargs) self.command = _command - self._name_parser_map = self.choices = _CommandDelegator(_command, self, **_runargs) + self._name_parser_map = self.choices = _CommandDelegator( + _command, self, **_runargs + ) class _CommandDelegator(cabc.MutableMapping): @@ -47,7 +49,9 @@ def __getitem__(self, k: str) -> ArgumentParser: if which(f'{self.command}-{k}'): return _DelegatingParser(self, k) # Only here is the command list retrieved - raise ArgumentError(self.action, f'No command “{k}”. Choose from {set(self)}') + raise ArgumentError( + self.action, f'No command “{k}”. Choose from {set(self)}' + ) def __setitem__(self, k: str, v: ArgumentParser) -> None: self.parser_map[k] = v @@ -70,7 +74,8 @@ def __hash__(self) -> int: def __eq__(self, other: Mapping[str, ArgumentParser]): if isinstance(other, _CommandDelegator): return all( - getattr(self, attr) == getattr(other, attr) for attr in ['command', 'action', 'parser_map', 'runargs'] + getattr(self, attr) == getattr(other, attr) + for attr in ['command', 'action', 'parser_map', 'runargs'] ) return self.parser_map == other @@ -98,7 +103,9 @@ def parse_known_args( args: Optional[Sequence[str]] = None, namespace: Optional[Namespace] = None, ) -> Tuple[Namespace, List[str]]: - assert args is not None and namespace is None, 'Only use DelegatingParser as subparser' + assert ( + args is not None and namespace is None + ), 'Only use DelegatingParser as subparser' return Namespace(func=partial(run, [self.prog, *args], **self.cd.runargs)), [] @@ -108,7 +115,9 @@ def _cmd_settings() -> None: print(settings) -def main(argv: Optional[Sequence[str]] = None, *, check: bool = True, **runargs) -> Optional[CompletedProcess]: +def main( + argv: Optional[Sequence[str]] = None, *, check: bool = True, **runargs +) -> Optional[CompletedProcess]: """\ Run a builtin scanpy command or a scanpy-* subcommand. @@ -116,7 +125,10 @@ def main(argv: Optional[Sequence[str]] = None, *, check: bool = True, **runargs) `~run(['scanpy', *argv], **runargs)` """ parser = ArgumentParser( - description=("There are a few packages providing commands. " "Try e.g. `pip install scanpy-scripts`!") + description=( + "There are a few packages providing commands. " + "Try e.g. `pip install scanpy-scripts`!" + ) ) parser.set_defaults(func=parser.print_help) diff --git a/scanpy/datasets/_datasets.py b/scanpy/datasets/_datasets.py index ec909b41e6..060b00ddd3 100644 --- a/scanpy/datasets/_datasets.py +++ b/scanpy/datasets/_datasets.py @@ -136,10 +136,13 @@ def moignard15() -> AnnData: } # annotate each observation/cell adata.obs['exp_groups'] = [ - next(gname for gname in groups.keys() if sname.startswith(gname)) for sname in adata.obs_names + next(gname for gname in groups.keys() if sname.startswith(gname)) + for sname in adata.obs_names ] # fix the order and colors of names in "groups" - adata.obs['exp_groups'] = pd.Categorical(adata.obs['exp_groups'], categories=list(groups.keys())) + adata.obs['exp_groups'] = pd.Categorical( + adata.obs['exp_groups'], categories=list(groups.keys()) + ) adata.uns['exp_groups_colors'] = list(groups.values()) return adata @@ -159,7 +162,10 @@ def paul15() -> AnnData: ------- Annotated data matrix. """ - logg.warning('In Scanpy 0.*, this returned logarithmized data. ' 'Now it returns non-logarithmized data.') + logg.warning( + 'In Scanpy 0.*, this returned logarithmized data. ' + 'Now it returns non-logarithmized data.' + ) import h5py filename = settings.datasetdir / 'paul15/paul15.h5' @@ -329,7 +335,9 @@ def _download_visium_dataset( # Download spatial data tar_filename = f"{sample_id}_spatial.tar.gz" tar_pth = sample_dir / tar_filename - _utils.check_presence_download(filename=tar_pth, backup_url=url_prefix + tar_filename) + _utils.check_presence_download( + filename=tar_pth, backup_url=url_prefix + tar_filename + ) with tarfile.open(tar_pth) as f: for el in f: if not (sample_dir / el.name).exists(): @@ -403,7 +411,9 @@ def visium_sge( spaceranger_version = "1.1.0" else: spaceranger_version = "1.2.0" - _download_visium_dataset(sample_id, spaceranger_version, download_image=include_hires_tiff) + _download_visium_dataset( + sample_id, spaceranger_version, download_image=include_hires_tiff + ) if include_hires_tiff: adata = read_visium( settings.datasetdir / sample_id, diff --git a/scanpy/datasets/_ebi_expression_atlas.py b/scanpy/datasets/_ebi_expression_atlas.py index e45e73a18d..013f0aa804 100644 --- a/scanpy/datasets/_ebi_expression_atlas.py +++ b/scanpy/datasets/_ebi_expression_atlas.py @@ -86,7 +86,9 @@ def read_expression_from_archive(archive: ZipFile) -> anndata.AnnData: return adata -def ebi_expression_atlas(accession: str, *, filter_boring: bool = False) -> anndata.AnnData: +def ebi_expression_atlas( + accession: str, *, filter_boring: bool = False +) -> anndata.AnnData: """\ Load a dataset from the `EBI Single Cell Expression Atlas `__ diff --git a/scanpy/external/exporting.py b/scanpy/external/exporting.py index 9a36dce538..a405cbb365 100644 --- a/scanpy/external/exporting.py +++ b/scanpy/external/exporting.py @@ -75,16 +75,25 @@ def spring_project( embedding_method = 'X_' + embedding_method else: if embedding_method in adata.uns: - embedding_method = 'X_' + embedding_method + '_' + adata.uns[embedding_method]['params']['layout'] + embedding_method = ( + 'X_' + + embedding_method + + '_' + + adata.uns[embedding_method]['params']['layout'] + ) else: - raise ValueError('Run the specified embedding method `%s` first.' % embedding_method) + raise ValueError( + 'Run the specified embedding method `%s` first.' % embedding_method + ) coords = adata.obsm[embedding_method] # Make project directory and subplot directory (subplot has same name as project) # For now, the subplot is just all cells in adata project_dir: Path = Path(project_dir) - subplot_dir: Path = project_dir.parent if subplot_name is None else project_dir / subplot_name + subplot_dir: Path = ( + project_dir.parent if subplot_name is None else project_dir / subplot_name + ) subplot_dir.mkdir(parents=True, exist_ok=True) print(f'Writing subplot to {subplot_dir}') @@ -150,7 +159,9 @@ def spring_project( elif is_categorical(adata.obs[obs_name]): categorical_extras[obs_name] = [str(x) for x in adata.obs[obs_name]] else: - logg.warning(f'Cell grouping {obs_name!r} is not a categorical variable') + logg.warning( + f'Cell grouping {obs_name!r} is not a categorical variable' + ) if custom_color_tracks is None: for obs_name in adata.obs: if not is_categorical(adata.obs[obs_name]): @@ -164,7 +175,9 @@ def spring_project( elif not is_categorical(adata.obs[obs_name]): continuous_extras[obs_name] = np.array(adata.obs[obs_name]) else: - logg.warning(f'Custom color track {obs_name!r} is not a continuous variable') + logg.warning( + f'Custom color track {obs_name!r} is not a continuous variable' + ) # Write continuous colors continuous_extras['Uniform'] = np.zeros(E.shape[0]) @@ -178,8 +191,12 @@ def spring_project( # Write categorical data categorical_coloring_data = {} - categorical_coloring_data = _build_categ_colors(categorical_coloring_data, categorical_extras) - _write_cell_groupings(subplot_dir / 'categorical_coloring_data.json', categorical_coloring_data) + categorical_coloring_data = _build_categ_colors( + categorical_coloring_data, categorical_extras + ) + _write_cell_groupings( + subplot_dir / 'categorical_coloring_data.json', categorical_coloring_data + ) # Write graph in two formats for backwards compatibility edges = _get_edges(adata, neighbors_key) @@ -193,7 +210,10 @@ def spring_project( # Write 2-D coordinates, after adjusting to roughly match SPRING's default d3js force layout parameters coords = coords - coords.min(0)[None, :] - coords = coords * (np.array([1000, 1000]) / coords.ptp(0))[None, :] + np.array([200, -200])[None, :] + coords = ( + coords * (np.array([1000, 1000]) / coords.ptp(0))[None, :] + + np.array([200, -200])[None, :] + ) np.savetxt( subplot_dir / 'coordinates.txt', np.hstack((np.arange(E.shape[0])[:, None], coords)), @@ -323,10 +343,14 @@ def _get_color_stats_genes(color_stats, E, gene_list): for iG in range(E.shape[1]): n_nonzero = E.indptr[iG + 1] - E.indptr[iG] if n_nonzero > pctl_n: - pctls[iG] = np.percentile(E.data[E.indptr[iG] : E.indptr[iG + 1]], 100 - 100 * pctl_n / n_nonzero) + pctls[iG] = np.percentile( + E.data[E.indptr[iG] : E.indptr[iG + 1]], 100 - 100 * pctl_n / n_nonzero + ) else: pctls[iG] = 0 - color_stats[gene_list[iG]] = tuple(map(float, (means[iG], stdevs[iG], mins[iG], maxes[iG], pctls[iG]))) + color_stats[gene_list[iG]] = tuple( + map(float, (means[iG], stdevs[iG], mins[iG], maxes[iG], pctls[iG])) + ) return color_stats @@ -348,7 +372,10 @@ def _write_color_stats(filename, color_stats): def _build_categ_colors(categorical_coloring_data, cell_groupings): for k, labels in cell_groupings.items(): - label_colors = {l: _frac_to_hex(float(i) / len(set(labels))) for i, l in enumerate(list(set(labels)))} + label_colors = { + l: _frac_to_hex(float(i) / len(set(labels))) + for i, l in enumerate(list(set(labels))) + } categorical_coloring_data[k] = { 'label_colors': label_colors, 'label_list': labels, @@ -358,7 +385,9 @@ def _build_categ_colors(categorical_coloring_data, cell_groupings): def _write_cell_groupings(filename, categorical_coloring_data): with open(filename, 'w') as f: - f.write(json.dumps(categorical_coloring_data, indent=4, sort_keys=True)) # .decode('utf-8')) + f.write( + json.dumps(categorical_coloring_data, indent=4, sort_keys=True) + ) # .decode('utf-8')) def _export_PAGA_to_SPRING(adata, paga_coords, outpath): @@ -369,14 +398,18 @@ def _export_PAGA_to_SPRING(adata, paga_coords, outpath): sizes = list(adata.uns[group_key + '_sizes']) clus_labels = adata.obs[group_key].cat.codes.values - cell_groups = [[int(j) for j in np.nonzero(clus_labels == i)[0]] for i in range(len(names))] + cell_groups = [ + [int(j) for j in np.nonzero(clus_labels == i)[0]] for i in range(len(names)) + ] if group_key + '_colors' in adata.uns: colors = list(adata.uns[group_key + '_colors']) else: import scanpy.plotting.utils - scanpy.plotting.utils.add_colors_for_categorical_sample_annotation(adata, group_key) + scanpy.plotting.utils.add_colors_for_categorical_sample_annotation( + adata, group_key + ) colors = list(adata.uns[group_key + '_colors']) # retrieve edge level data @@ -398,7 +431,9 @@ def _export_PAGA_to_SPRING(adata, paga_coords, outpath): # make node list nodes = [] - for i, name, xy, color, size, cells in zip(range(len(names)), names, coords, colors, sizes, cell_groups): + for i, name, xy, color, size, cells in zip( + range(len(names)), names, coords, colors, sizes, cell_groups + ): nodes.append( { 'index': i, @@ -414,7 +449,9 @@ def _export_PAGA_to_SPRING(adata, paga_coords, outpath): links = [] for source, target, weight in zip(sources, targets, weights): if source < target and weight > min_edge_weight_save: - links.append({'source': int(source), 'target': int(target), 'weight': float(weight)}) + links.append( + {'source': int(source), 'target': int(target), 'weight': float(weight)} + ) # save data about edge weights edge_weight_meta = { @@ -527,7 +564,10 @@ def cellbrowser( try: import cellbrowser.cellbrowser as cb except ImportError: - logg.error("The package cellbrowser is not installed. " "Install with 'pip install cellbrowser' and retry.") + logg.error( + "The package cellbrowser is not installed. " + "Install with 'pip install cellbrowser' and retry." + ) raise data_dir = str(data_dir) diff --git a/scanpy/external/pl.py b/scanpy/external/pl.py index 01fac05ed6..2c4167d72f 100644 --- a/scanpy/external/pl.py +++ b/scanpy/external/pl.py @@ -176,7 +176,9 @@ def sam( try: dt = adata.obsm[projection] except KeyError: - raise ValueError('Please create a projection first using run_umap or run_tsne') + raise ValueError( + 'Please create a projection first using run_umap or run_tsne' + ) else: dt = projection @@ -185,7 +187,9 @@ def sam( axes = plt.gca() if c is None: - axes.scatter(dt[:, 0], dt[:, 1], s=s, linewidth=linewidth, edgecolor=edgecolor, **kwargs) + axes.scatter( + dt[:, 0], dt[:, 1], s=s, linewidth=linewidth, edgecolor=edgecolor, **kwargs + ) return axes if isinstance(c, str): diff --git a/scanpy/external/pp/_hashsolo.py b/scanpy/external/pp/_hashsolo.py index ca2ab5179a..ecc3e8e9ac 100644 --- a/scanpy/external/pp/_hashsolo.py +++ b/scanpy/external/pp/_hashsolo.py @@ -66,7 +66,9 @@ def gaussian_updates(data, mu_o, std_o): n = len(data) lam = 1 / np.var(data) if len(data) > 1 else lam_o lam_n = lam_o + n * lam - mu_n = (np.mean(data) * n * lam + mu_o * lam_o) / lam_n if len(data) > 0 else mu_o + mu_n = ( + (np.mean(data) * n * lam + mu_o * lam_o) / lam_n if len(data) > 0 else mu_o + ) return mu_n, (1 / (lam_n / (n + 1))) ** (1 / 2) eps = 1e-15 @@ -89,8 +91,12 @@ def gaussian_updates(data, mu_o, std_o): # barcodes with rank < k are considered to be noise global_signal_counts = np.ravel(data_sort[:, -1]) global_noise_counts = np.ravel(data_sort[:, :-number_of_non_noise_barcodes]) - global_mu_signal_o, global_sigma_signal_o = np.mean(global_signal_counts), np.std(global_signal_counts) - global_mu_noise_o, global_sigma_noise_o = np.mean(global_noise_counts), np.std(global_noise_counts) + global_mu_signal_o, global_sigma_signal_o = np.mean(global_signal_counts), np.std( + global_signal_counts + ) + global_mu_noise_o, global_sigma_noise_o = np.mean(global_noise_counts), np.std( + global_noise_counts + ) noise_params_dict = {} signal_params_dict = {} @@ -98,7 +104,9 @@ def gaussian_updates(data, mu_o, std_o): # for each barcode get empirical noise and signal distribution parameterization for x in np.arange(num_of_barcodes): sample_barcodes = data[:, x] - sample_barcodes_noise_idx = np.where(data_arg[:, :num_of_noise_barcodes] == x)[0] + sample_barcodes_noise_idx = np.where(data_arg[:, :num_of_noise_barcodes] == x)[ + 0 + ] sample_barcodes_signal_idx = np.where(data_arg[:, -1] == x) # get noise and signal counts @@ -106,8 +114,12 @@ def gaussian_updates(data, mu_o, std_o): signal_counts = sample_barcodes[sample_barcodes_signal_idx] # get parameters of distribution, assuming lognormal do update from global values - noise_param = gaussian_updates(noise_counts, global_mu_noise_o, global_sigma_noise_o) - signal_param = gaussian_updates(signal_counts, global_mu_signal_o, global_sigma_signal_o) + noise_param = gaussian_updates( + noise_counts, global_mu_noise_o, global_sigma_noise_o + ) + signal_param = gaussian_updates( + signal_counts, global_mu_signal_o, global_sigma_signal_o + ) noise_params_dict[x] = noise_param signal_params_dict[x] = signal_param @@ -115,7 +127,9 @@ def gaussian_updates(data, mu_o, std_o): counter = 0 # for each combination of noise and signal barcode calculate probiltiy of in silico and real cell hypotheses - for noise_sample_idx, signal_sample_idx in product(np.arange(num_of_barcodes), np.arange(num_of_barcodes)): + for noise_sample_idx, signal_sample_idx in product( + np.arange(num_of_barcodes), np.arange(num_of_barcodes) + ): signal_subset = data_arg[:, -1] == signal_sample_idx noise_subset = data_arg[:, -2] == noise_sample_idx subset = signal_subset & noise_subset @@ -168,9 +182,15 @@ def gaussian_updates(data, mu_o, std_o): + eps ) - probs_of_negative = np.sum([log_noise_noise_probs, log_signal_noise_probs], axis=0) - probs_of_singlet = np.sum([log_noise_noise_probs, log_signal_signal_probs], axis=0) - probs_of_doublet = np.sum([log_noise_signal_probs, log_signal_signal_probs], axis=0) + probs_of_negative = np.sum( + [log_noise_noise_probs, log_signal_noise_probs], axis=0 + ) + probs_of_singlet = np.sum( + [log_noise_noise_probs, log_signal_signal_probs], axis=0 + ) + probs_of_doublet = np.sum( + [log_noise_signal_probs, log_signal_signal_probs], axis=0 + ) log_probs_list = [probs_of_negative, probs_of_singlet, probs_of_doublet] # each cell and each hypothesis probability @@ -206,11 +226,15 @@ def _calculate_bayes_rule(data, priors, number_of_noise_barcodes): "log_likelihoods_for_each_hypothesis" key is a 2d np.array log likelihood of each hypothesis """ priors = np.array(priors) - log_likelihoods_for_each_hypothesis, _, _ = _calculate_log_likelihoods(data, number_of_noise_barcodes) + log_likelihoods_for_each_hypothesis, _, _ = _calculate_log_likelihoods( + data, number_of_noise_barcodes + ) probs_hypotheses = ( np.exp(log_likelihoods_for_each_hypothesis) * priors - / np.sum(np.multiply(np.exp(log_likelihoods_for_each_hypothesis), priors), axis=1)[:, None] + / np.sum( + np.multiply(np.exp(log_likelihoods_for_each_hypothesis), priors), axis=1 + )[:, None] ) most_likely_hypothesis = np.argmax(probs_hypotheses, axis=1) return { @@ -271,7 +295,9 @@ def hashsolo( >>> sce.pp.hashsolo(data, ['Hash1', 'Hash2', 'Hash3']) >>> data.obs.head() """ - print("Please cite HashSolo paper:\nhttps://www.cell.com/cell-systems/fulltext/S2405-4712(20)30195-2") + print( + "Please cite HashSolo paper:\nhttps://www.cell.com/cell-systems/fulltext/S2405-4712(20)30195-2" + ) data = adata.obs[cell_hashing_columns].values if not check_nonnegative_integers(data): @@ -300,39 +326,67 @@ def hashsolo( unique_cluster_features = np.unique(adata.obs[cluster_features]) for cluster_feature in unique_cluster_features: cluster_feature_bool_vector = adata.obs[cluster_features] == cluster_feature - posterior_dict = _calculate_bayes_rule(data[cluster_feature_bool_vector], priors, number_of_noise_barcodes) - results.loc[cluster_feature_bool_vector, "most_likely_hypothesis"] = posterior_dict[ - "most_likely_hypothesis" - ] - results.loc[cluster_feature_bool_vector, "cluster_feature"] = cluster_feature - results.loc[cluster_feature_bool_vector, "negative_hypothesis_probability"] = posterior_dict[ - "probs_hypotheses" - ][:, 0] - results.loc[cluster_feature_bool_vector, "singlet_hypothesis_probability"] = posterior_dict[ - "probs_hypotheses" - ][:, 1] - results.loc[cluster_feature_bool_vector, "doublet_hypothesis_probability"] = posterior_dict[ - "probs_hypotheses" - ][:, 2] + posterior_dict = _calculate_bayes_rule( + data[cluster_feature_bool_vector], priors, number_of_noise_barcodes + ) + results.loc[ + cluster_feature_bool_vector, "most_likely_hypothesis" + ] = posterior_dict["most_likely_hypothesis"] + results.loc[ + cluster_feature_bool_vector, "cluster_feature" + ] = cluster_feature + results.loc[ + cluster_feature_bool_vector, "negative_hypothesis_probability" + ] = posterior_dict["probs_hypotheses"][:, 0] + results.loc[ + cluster_feature_bool_vector, "singlet_hypothesis_probability" + ] = posterior_dict["probs_hypotheses"][:, 1] + results.loc[ + cluster_feature_bool_vector, "doublet_hypothesis_probability" + ] = posterior_dict["probs_hypotheses"][:, 2] else: posterior_dict = _calculate_bayes_rule(data, priors, number_of_noise_barcodes) - results.loc[:, "most_likely_hypothesis"] = posterior_dict["most_likely_hypothesis"] + results.loc[:, "most_likely_hypothesis"] = posterior_dict[ + "most_likely_hypothesis" + ] results.loc[:, "cluster_feature"] = 0 - results.loc[:, "negative_hypothesis_probability"] = posterior_dict["probs_hypotheses"][:, 0] - results.loc[:, "singlet_hypothesis_probability"] = posterior_dict["probs_hypotheses"][:, 1] - results.loc[:, "doublet_hypothesis_probability"] = posterior_dict["probs_hypotheses"][:, 2] - - adata.obs["most_likely_hypothesis"] = results.loc[adata.obs_names, "most_likely_hypothesis"] + results.loc[:, "negative_hypothesis_probability"] = posterior_dict[ + "probs_hypotheses" + ][:, 0] + results.loc[:, "singlet_hypothesis_probability"] = posterior_dict[ + "probs_hypotheses" + ][:, 1] + results.loc[:, "doublet_hypothesis_probability"] = posterior_dict[ + "probs_hypotheses" + ][:, 2] + + adata.obs["most_likely_hypothesis"] = results.loc[ + adata.obs_names, "most_likely_hypothesis" + ] adata.obs["cluster_feature"] = results.loc[adata.obs_names, "cluster_feature"] - adata.obs["negative_hypothesis_probability"] = results.loc[adata.obs_names, "negative_hypothesis_probability"] - adata.obs["singlet_hypothesis_probability"] = results.loc[adata.obs_names, "singlet_hypothesis_probability"] - adata.obs["doublet_hypothesis_probability"] = results.loc[adata.obs_names, "doublet_hypothesis_probability"] + adata.obs["negative_hypothesis_probability"] = results.loc[ + adata.obs_names, "negative_hypothesis_probability" + ] + adata.obs["singlet_hypothesis_probability"] = results.loc[ + adata.obs_names, "singlet_hypothesis_probability" + ] + adata.obs["doublet_hypothesis_probability"] = results.loc[ + adata.obs_names, "doublet_hypothesis_probability" + ] adata.obs["Classification"] = None - adata.obs.loc[adata.obs["most_likely_hypothesis"] == 2, "Classification"] = "Doublet" - adata.obs.loc[adata.obs["most_likely_hypothesis"] == 0, "Classification"] = "Negative" + adata.obs.loc[ + adata.obs["most_likely_hypothesis"] == 2, "Classification" + ] = "Doublet" + adata.obs.loc[ + adata.obs["most_likely_hypothesis"] == 0, "Classification" + ] = "Negative" all_sings = adata.obs["most_likely_hypothesis"] == 1 - singlet_sample_index = np.argmax(adata.obs.loc[all_sings, cell_hashing_columns].values, axis=1) - adata.obs.loc[all_sings, "Classification"] = adata.obs[cell_hashing_columns].columns[singlet_sample_index] + singlet_sample_index = np.argmax( + adata.obs.loc[all_sings, cell_hashing_columns].values, axis=1 + ) + adata.obs.loc[all_sings, "Classification"] = adata.obs[ + cell_hashing_columns + ].columns[singlet_sample_index] return adata if not inplace else None diff --git a/scanpy/external/pp/_magic.py b/scanpy/external/pp/_magic.py index aeb382733b..5690923c5b 100644 --- a/scanpy/external/pp/_magic.py +++ b/scanpy/external/pp/_magic.py @@ -150,7 +150,10 @@ def magic( start = logg.info('computing MAGIC') all_or_pca = isinstance(name_list, (str, type(None))) if all_or_pca and name_list not in {"all_genes", "pca_only", None}: - raise ValueError("Invalid string value for `name_list`: " "Only `'all_genes'` and `'pca_only'` are allowed.") + raise ValueError( + "Invalid string value for `name_list`: " + "Only `'all_genes'` and `'pca_only'` are allowed." + ) if copy is None: copy = not all_or_pca elif not all_or_pca and not copy: @@ -178,7 +181,11 @@ def magic( logg.info( ' finished', time=start, - deep=("added\n 'X_magic', PCA on MAGIC coordinates (adata.obsm)" if name_list == "pca_only" else ''), + deep=( + "added\n 'X_magic', PCA on MAGIC coordinates (adata.obsm)" + if name_list == "pca_only" + else '' + ), ) # update AnnData instance if name_list == "pca_only": diff --git a/scanpy/external/pp/_mnn_correct.py b/scanpy/external/pp/_mnn_correct.py index ff90cb05be..67ff5748ba 100644 --- a/scanpy/external/pp/_mnn_correct.py +++ b/scanpy/external/pp/_mnn_correct.py @@ -28,7 +28,11 @@ def mnn_correct( save_raw: bool = False, n_jobs: Optional[int] = None, **kwargs, -) -> Tuple[Union[np.ndarray, AnnData], List[pd.DataFrame], Optional[List[Tuple[Optional[float], int]]],]: +) -> Tuple[ + Union[np.ndarray, AnnData], + List[pd.DataFrame], + Optional[List[Tuple[Optional[float], int]]], +]: """\ Correct batch effects by matching mutual nearest neighbors [Haghverdi18]_ [Kang18]_. @@ -122,7 +126,10 @@ def mnn_correct( try: from mnnpy import mnn_correct except ImportError: - raise ImportError('Please install the package mnnpy ' '(https://github.com/chriscainx/mnnpy). ') + raise ImportError( + 'Please install the package mnnpy ' + '(https://github.com/chriscainx/mnnpy). ' + ) n_jobs = settings.n_jobs if n_jobs is None else n_jobs datas, mnn_list, angle_list = mnn_correct( diff --git a/scanpy/external/pp/_scanorama_integrate.py b/scanpy/external/pp/_scanorama_integrate.py index 0d5a058ec1..9ea4d150cd 100644 --- a/scanpy/external/pp/_scanorama_integrate.py +++ b/scanpy/external/pp/_scanorama_integrate.py @@ -113,7 +113,9 @@ def scanorama_integrate( name2idx[batch_name].append(idx) # Separate batches. - datasets_dimred = [adata.obsm[basis][name2idx[batch_name]] for batch_name in batch_names] + datasets_dimred = [ + adata.obsm[basis][name2idx[batch_name]] for batch_name in batch_names + ] # Integrate. integrated = scanorama.assemble( diff --git a/scanpy/external/pp/_scrublet.py b/scanpy/external/pp/_scrublet.py index e2aabb5834..6611fccac5 100644 --- a/scanpy/external/pp/_scrublet.py +++ b/scanpy/external/pp/_scrublet.py @@ -150,7 +150,9 @@ def scrublet( try: import scrublet as sl except ImportError: - raise ImportError('Please install scrublet: `pip install scrublet` or `conda install scrublet`.') + raise ImportError( + 'Please install scrublet: `pip install scrublet` or `conda install scrublet`.' + ) if copy: adata = adata.copy() @@ -340,7 +342,9 @@ def _scrublet_call_doublets( try: import scrublet as sl except ImportError: - raise ImportError('Please install scrublet: `pip install scrublet` or `conda install scrublet`.') + raise ImportError( + 'Please install scrublet: `pip install scrublet` or `conda install scrublet`.' + ) # Estimate n_neighbors if not provided, and create scrublet object. @@ -381,10 +385,14 @@ def _scrublet_call_doublets( if mean_center: logg.info('Embedding transcriptomes using PCA...') - sl.pipeline_pca(scrub, n_prin_comps=n_prin_comps, random_state=scrub.random_state) + sl.pipeline_pca( + scrub, n_prin_comps=n_prin_comps, random_state=scrub.random_state + ) else: logg.info('Embedding transcriptomes using Truncated SVD...') - sl.pipeline_truncated_svd(scrub, n_prin_comps=n_prin_comps, random_state=scrub.random_state) + sl.pipeline_truncated_svd( + scrub, n_prin_comps=n_prin_comps, random_state=scrub.random_state + ) # Score the doublets @@ -412,7 +420,9 @@ def _scrublet_call_doublets( 'parameters': { 'expected_doublet_rate': expected_doublet_rate, 'sim_doublet_ratio': ( - adata_sim.uns.get('scrublet', {}).get('parameters', {}).get('sim_doublet_ratio', None) + adata_sim.uns.get('scrublet', {}) + .get('parameters', {}) + .get('sim_doublet_ratio', None) ), 'n_neighbors': n_neighbors, 'random_state': random_state, @@ -420,7 +430,9 @@ def _scrublet_call_doublets( } if get_doublet_neighbor_parents: - adata_obs.uns['scrublet']['doublet_neighbor_parents'] = scrub.doublet_neighbor_parents_ + adata_obs.uns['scrublet'][ + 'doublet_neighbor_parents' + ] = scrub.doublet_neighbor_parents_ return adata_obs @@ -475,7 +487,9 @@ def scrublet_simulate_doublets( try: import scrublet as sl except ImportError: - raise ImportError('Please install scrublet: `pip install scrublet` or `conda install scrublet`.') + raise ImportError( + 'Please install scrublet: `pip install scrublet` or `conda install scrublet`.' + ) X = _get_obs_rep(adata, layer=layer) scrub = sl.Scrublet(X) diff --git a/scanpy/external/tl/_palantir.py b/scanpy/external/tl/_palantir.py index 8fc05de277..ba437d5f52 100644 --- a/scanpy/external/tl/_palantir.py +++ b/scanpy/external/tl/_palantir.py @@ -217,7 +217,9 @@ def palantir( # MAGIC imputation if impute_data: - imp_df = run_magic_imputation(data=adata.to_df(), dm_res=dm_res, n_steps=n_steps) + imp_df = run_magic_imputation( + data=adata.to_df(), dm_res=dm_res, n_steps=n_steps + ) adata.layers['palantir_imp'] = imp_df ( diff --git a/scanpy/external/tl/_phate.py b/scanpy/external/tl/_phate.py index f4e83d65fe..0e077b429e 100644 --- a/scanpy/external/tl/_phate.py +++ b/scanpy/external/tl/_phate.py @@ -130,7 +130,8 @@ def phate( import phate except ImportError: raise ImportError( - 'You need to install the package `phate`: please run `pip install ' '--user phate` in a terminal.' + 'You need to install the package `phate`: please run `pip install ' + '--user phate` in a terminal.' ) X_phate = phate.PHATE( n_components=n_components, diff --git a/scanpy/external/tl/_phenograph.py b/scanpy/external/tl/_phenograph.py index ef25c19e20..2a956518a2 100644 --- a/scanpy/external/tl/_phenograph.py +++ b/scanpy/external/tl/_phenograph.py @@ -195,7 +195,10 @@ def phenograph( assert phenograph.__version__ >= "1.5.3" except (ImportError, AssertionError, AttributeError): - raise ImportError("please install the latest release of phenograph:\n\t" "pip install -U PhenoGraph") + raise ImportError( + "please install the latest release of phenograph:\n\t" + "pip install -U PhenoGraph" + ) if isinstance(adata, AnnData): try: @@ -206,7 +209,11 @@ def phenograph( data = adata copy = True - comm_key = "pheno_{}".format(clustering_algo) if clustering_algo in ["louvain", "leiden"] else '' + comm_key = ( + "pheno_{}".format(clustering_algo) + if clustering_algo in ["louvain", "leiden"] + else '' + ) ig_key = "pheno_{}_ig".format("jaccard" if jaccard else "gaussian") q_key = "pheno_{}_q".format("jaccard" if jaccard else "gaussian") diff --git a/scanpy/external/tl/_trimap.py b/scanpy/external/tl/_trimap.py index 89d636e87e..5334e29fda 100644 --- a/scanpy/external/tl/_trimap.py +++ b/scanpy/external/tl/_trimap.py @@ -101,7 +101,8 @@ def trimap( X = adata.X if scp.issparse(X): raise ValueError( - 'trimap currently does not support sparse matrices. Please' 'use a dense matrix or apply pca first.' + 'trimap currently does not support sparse matrices. Please' + 'use a dense matrix or apply pca first.' ) logg.warning('`X_pca` not found. Run `sc.pp.pca` first for speedup.') X_trimap = TRIMAP( diff --git a/scanpy/external/tl/_wishbone.py b/scanpy/external/tl/_wishbone.py index c8f3415b58..681950a264 100644 --- a/scanpy/external/tl/_wishbone.py +++ b/scanpy/external/tl/_wishbone.py @@ -93,16 +93,24 @@ def wishbone( try: from wishbone.core import wishbone as c_wishbone except ImportError: - raise ImportError("\nplease install wishbone:\n\n\thttps://github.com/dpeerlab/wishbone") + raise ImportError( + "\nplease install wishbone:\n\n\thttps://github.com/dpeerlab/wishbone" + ) # Start cell index s = np.where(adata.obs_names == start_cell)[0] if len(s) == 0: - raise RuntimeError(f"Start cell {start_cell} not found in data. " "Please rerun with correct start cell.") + raise RuntimeError( + f"Start cell {start_cell} not found in data. " + "Please rerun with correct start cell." + ) if isinstance(num_waypoints, cabc.Collection): diff = np.setdiff1d(num_waypoints, adata.obs.index) if diff.size > 0: - logging.warning("Some of the specified waypoints are not in the data. " "These will be removed") + logging.warning( + "Some of the specified waypoints are not in the data. " + "These will be removed" + ) num_waypoints = diff.tolist() elif num_waypoints > adata.shape[0]: raise RuntimeError( @@ -124,7 +132,9 @@ def wishbone( # Assign results trajectory = res["Trajectory"] - trajectory = (trajectory - np.min(trajectory)) / (np.max(trajectory) - np.min(trajectory)) + trajectory = (trajectory - np.min(trajectory)) / ( + np.max(trajectory) - np.min(trajectory) + ) adata.obs['trajectory_wishbone'] = np.asarray(trajectory) # branch_ = None @@ -137,7 +147,9 @@ def _anndata_to_wishbone(adata: AnnData): from wishbone.wb import SCData, Wishbone scdata = SCData(adata.to_df()) - scdata.diffusion_eigenvectors = pd.DataFrame(adata.obsm['X_diffmap'], index=adata.obs_names) + scdata.diffusion_eigenvectors = pd.DataFrame( + adata.obsm['X_diffmap'], index=adata.obs_names + ) wb = Wishbone(scdata) wb.trajectory = adata.obs["trajectory_wishbone"] wb.branch = adata.obs["branch_wishbone"] diff --git a/scanpy/get/get.py b/scanpy/get/get.py index 5f1f818dd7..10cde3f9e5 100644 --- a/scanpy/get/get.py +++ b/scanpy/get/get.py @@ -147,21 +147,26 @@ def _check_indices( if key in dim_df.columns: col_keys.append(key) if key in alt_names.index: - raise KeyError(f"The key '{key}' is found in both adata.{dim} and {alt_repr}.{alt_search_repr}.") + raise KeyError( + f"The key '{key}' is found in both adata.{dim} and {alt_repr}.{alt_search_repr}." + ) elif key in alt_names.index: val = alt_names[key] if isinstance(val, pd.Series): # while var_names must be unique, adata.var[gene_symbols] does not # It's still ambiguous to refer to a duplicated entry though. assert alias_index is not None - raise KeyError(f"Found duplicate entries for '{key}' in {alt_repr}.{alt_search_repr}.") + raise KeyError( + f"Found duplicate entries for '{key}' in {alt_repr}.{alt_search_repr}." + ) index_keys.append(val) index_aliases.append(key) else: not_found.append(key) if len(not_found) > 0: raise KeyError( - f"Could not find keys '{not_found}' in columns of `adata.{dim}` or in" f" {alt_repr}.{alt_search_repr}." + f"Could not find keys '{not_found}' in columns of `adata.{dim}` or in" + f" {alt_repr}.{alt_search_repr}." ) return col_keys, index_keys, index_aliases @@ -253,7 +258,9 @@ def obs_df( >>> mean, var = grouped.mean(), grouped.var() """ if use_raw: - assert layer is None, "Cannot specify use_raw=True and a layer at the same time." + assert ( + layer is None + ), "Cannot specify use_raw=True and a layer at the same time." var = adata.raw.var else: var = adata.var @@ -400,7 +407,8 @@ def _get_obs_rep(adata, *, use_raw=False, layer=None, obsm=None, obsp=None): return adata.obsp[obsp] else: assert False, ( - "That was unexpected. Please report this bug at:\n\n\t" " https://github.com/theislab/scanpy/issues" + "That was unexpected. Please report this bug at:\n\n\t" + " https://github.com/theislab/scanpy/issues" ) @@ -426,5 +434,6 @@ def _set_obs_rep(adata, val, *, use_raw=False, layer=None, obsm=None, obsp=None) adata.obsp[obsp] = val else: assert False, ( - "That was unexpected. Please report this bug at:\n\n\t" " https://github.com/theislab/scanpy/issues" + "That was unexpected. Please report this bug at:\n\n\t" + " https://github.com/theislab/scanpy/issues" ) diff --git a/scanpy/logging.py b/scanpy/logging.py index 8da4477542..c0f810b281 100644 --- a/scanpy/logging.py +++ b/scanpy/logging.py @@ -84,7 +84,9 @@ def _set_log_level(settings, level: int): class _LogFormatter(logging.Formatter): - def __init__(self, fmt='{levelname}: {message}', datefmt='%Y-%m-%d %H:%M', style='{'): + def __init__( + self, fmt='{levelname}: {message}', datefmt='%Y-%m-%d %H:%M', style='{' + ): super().__init__(fmt, datefmt, style) def format(self, record: logging.LogRecord): @@ -98,9 +100,13 @@ def format(self, record: logging.LogRecord): if record.time_passed: # strip microseconds if record.time_passed.microseconds: - record.time_passed = timedelta(seconds=int(record.time_passed.total_seconds())) + record.time_passed = timedelta( + seconds=int(record.time_passed.total_seconds()) + ) if '{time_passed}' in record.msg: - record.msg = record.msg.replace('{time_passed}', str(record.time_passed)) + record.msg = record.msg.replace( + '{time_passed}', str(record.time_passed) + ) else: self._style._fmt += ' ({time_passed})' if record.deep: diff --git a/scanpy/neighbors/__init__.py b/scanpy/neighbors/__init__.py index dc3dc8c582..6faf932e20 100644 --- a/scanpy/neighbors/__init__.py +++ b/scanpy/neighbors/__init__.py @@ -23,7 +23,9 @@ _Method = Literal['umap', 'gauss', 'rapids'] _MetricFn = Callable[[np.ndarray, np.ndarray], float] # from sklearn.metrics.pairwise_distances.__doc__: -_MetricSparseCapable = Literal['cityblock', 'cosine', 'euclidean', 'l1', 'l2', 'manhattan'] +_MetricSparseCapable = Literal[ + 'cityblock', 'cosine', 'euclidean', 'l1', 'l2', 'manhattan' +] _MetricScipySpatial = Literal[ 'braycurtis', 'canberra', @@ -336,7 +338,9 @@ def compute_neighbors_rapids(X: np.ndarray, n_neighbors: int): return knn_indices, np.sqrt(knn_distsq) # cuml uses sqeuclidean metric so take sqrt -def _get_sparse_matrix_from_indices_distances_umap(knn_indices, knn_dists, n_obs, n_neighbors): +def _get_sparse_matrix_from_indices_distances_umap( + knn_indices, knn_dists, n_obs, n_neighbors +): rows = np.zeros((n_obs * n_neighbors), dtype=np.int64) cols = np.zeros((n_obs * n_neighbors), dtype=np.int64) vals = np.zeros((n_obs * n_neighbors), dtype=np.float64) @@ -398,12 +402,16 @@ def _compute_connectivities_umap( # In umap-learn 0.4, this returns (result, sigmas, rhos) connectivities = connectivities[0] - distances = _get_sparse_matrix_from_indices_distances_umap(knn_indices, knn_dists, n_obs, n_neighbors) + distances = _get_sparse_matrix_from_indices_distances_umap( + knn_indices, knn_dists, n_obs, n_neighbors + ) return distances, connectivities.tocsr() -def _get_sparse_matrix_from_indices_distances_numpy(indices, distances, n_obs, n_neighbors): +def _get_sparse_matrix_from_indices_distances_numpy( + indices, distances, n_obs, n_neighbors +): n_nonzero = n_obs * n_neighbors indptr = np.arange(0, n_nonzero + 1, n_neighbors) D = csr_matrix( @@ -432,7 +440,9 @@ def _get_indices_distances_from_sparse_matrix(D, n_neighbors: int): if len(neighbors[1]) > n_neighbors_m1: sorted_indices = np.argsort(D[i][neighbors].A1)[:n_neighbors_m1] indices[i, 1:] = neighbors[1][sorted_indices] - distances[i, 1:] = D[i][neighbors[0][sorted_indices], neighbors[1][sorted_indices]] + distances[i, 1:] = D[i][ + neighbors[0][sorted_indices], neighbors[1][sorted_indices] + ] else: indices[i, 1:] = neighbors[1] distances[i, 1:] = D[i][neighbors] @@ -466,7 +476,9 @@ def _make_forest_dict(forest): props = ('hyperplanes', 'offsets', 'children', 'indices') for prop in props: d[prop] = {} - sizes = np.fromiter((getattr(tree, prop).shape[0] for tree in forest), dtype=int) + sizes = np.fromiter( + (getattr(tree, prop).shape[0] for tree in forest), dtype=int + ) d[prop]['start'] = np.zeros_like(sizes) if prop == 'offsets': dims = sizes.sum() @@ -594,9 +606,15 @@ def count_nonzero(a: Union[np.ndarray, csr_matrix]) -> int: # estimating n_neighbors if self._connectivities is None: - self.n_neighbors = int(count_nonzero(self._distances) / self._distances.shape[0]) + self.n_neighbors = int( + count_nonzero(self._distances) / self._distances.shape[0] + ) else: - self.n_neighbors = int(count_nonzero(self._connectivities) / self._connectivities.shape[0] / 2) + self.n_neighbors = int( + count_nonzero(self._connectivities) + / self._connectivities.shape[0] + / 2 + ) info_str += '`.distances` `.connectivities` ' self._number_connected_components = 1 if issparse(self._connectivities): @@ -611,7 +629,9 @@ def count_nonzero(a: Union[np.ndarray, csr_matrix]) -> int: if n_dcs > len(self._eigen_values): raise ValueError( 'Cannot instantiate using `n_dcs`={}. ' - 'Compute diffmap/spectrum with more components first.'.format(n_dcs) + 'Compute diffmap/spectrum with more components first.'.format( + n_dcs + ) ) self._eigen_values = self._eigen_values[:n_dcs] self._eigen_basis = self._eigen_basis[:, :n_dcs] @@ -736,7 +756,9 @@ def compute_neighbors( if method == 'umap' and not knn: raise ValueError('`method = \'umap\' only with `knn = True`.') if method == 'rapids' and metric != 'euclidean': - raise ValueError("`method` 'rapids' only supports the 'euclidean' `metric`.") + raise ValueError( + "`method` 'rapids' only supports the 'euclidean' `metric`." + ) if method not in {'umap', 'gauss', 'rapids'}: raise ValueError("`method` needs to be 'umap', 'gauss', or 'rapids'.") if self._adata.shape[0] >= 10000 and not knn: @@ -747,10 +769,14 @@ def compute_neighbors( self.knn = knn X = _choose_representation(self._adata, use_rep=use_rep, n_pcs=n_pcs) # neighbor search - use_dense_distances = (metric == 'euclidean' and X.shape[0] < 8192) or knn == False + use_dense_distances = ( + metric == 'euclidean' and X.shape[0] < 8192 + ) or knn == False if use_dense_distances: _distances = pairwise_distances(X, metric=metric, **metric_kwds) - knn_indices, knn_distances = _get_indices_distances_from_dense_matrix(_distances, n_neighbors) + knn_indices, knn_distances = _get_indices_distances_from_dense_matrix( + _distances, n_neighbors + ) if knn: self._distances = _get_sparse_matrix_from_indices_distances_numpy( knn_indices, knn_distances, X.shape[0], n_neighbors @@ -803,10 +829,14 @@ def _compute_connectivities_diffmap(self, density_normalize=True): # init distances if self.knn: Dsq = self._distances.power(2) - indices, distances_sq = _get_indices_distances_from_sparse_matrix(Dsq, self.n_neighbors) + indices, distances_sq = _get_indices_distances_from_sparse_matrix( + Dsq, self.n_neighbors + ) else: Dsq = np.power(self._distances, 2) - indices, distances_sq = _get_indices_distances_from_dense_matrix(Dsq, self.n_neighbors) + indices, distances_sq = _get_indices_distances_from_dense_matrix( + Dsq, self.n_neighbors + ) # exclude the first point, the 0th neighbor indices = indices[:, 1:] @@ -846,7 +876,9 @@ def _compute_connectivities_diffmap(self, density_normalize=True): # set all entries that are not nearest neighbors to zero W[mask == False] = 0 else: - W = Dsq.copy() # need to copy the distance matrix here; what follows is inplace + W = ( + Dsq.copy() + ) # need to copy the distance matrix here; what follows is inplace for i in range(len(Dsq.indptr[:-1])): row = Dsq.indices[Dsq.indptr[i] : Dsq.indptr[i + 1]] num = 2 * sigmas[i] * sigmas[row] @@ -948,12 +980,17 @@ def compute_eigen( which = 'LM' if sort == 'decrease' else 'SM' # it pays off to increase the stability with a bit more precision matrix = matrix.astype(np.float64) - evals, evecs = scipy.sparse.linalg.eigsh(matrix, k=n_comps, which=which, ncv=ncv) + evals, evecs = scipy.sparse.linalg.eigsh( + matrix, k=n_comps, which=which, ncv=ncv + ) evals, evecs = evals.astype(np.float32), evecs.astype(np.float32) if sort == 'decrease': evals = evals[::-1] evecs = evecs[:, ::-1] - logg.info(' eigenvalues of transition matrix\n' ' {}'.format(str(evals).replace('\n', '\n '))) + logg.info( + ' eigenvalues of transition matrix\n' + ' {}'.format(str(evals).replace('\n', '\n ')) + ) if self._number_connected_components > len(evals) / 2: logg.warning('Transition matrix has many disconnected components!') self._eigen_values = evals @@ -987,7 +1024,12 @@ def _get_dpt_row(self, i): label = self._connected_components[1][i] mask = self._connected_components[1] == label row = sum( - (self.eigen_values[l] / (1 - self.eigen_values[l]) * (self.eigen_basis[i, l] - self.eigen_basis[:, l])) ** 2 + ( + self.eigen_values[l] + / (1 - self.eigen_values[l]) + * (self.eigen_basis[i, l] - self.eigen_basis[:, l]) + ) + ** 2 # account for float32 precision for l in range(0, self.eigen_values.size) if self.eigen_values[l] < 0.9994 @@ -1024,7 +1066,9 @@ def _set_iroot_via_xroot(self, xroot): condition, only relevant for computing pseudotime. """ if self._adata.shape[1] != xroot.size: - raise ValueError('The root vector you provided does not have the ' 'correct dimension.') + raise ValueError( + 'The root vector you provided does not have the ' 'correct dimension.' + ) # this is the squared distance dsqroot = 1e10 iroot = 0 diff --git a/scanpy/plotting/_anndata.py b/scanpy/plotting/_anndata.py index af5754ac72..6c319682ce 100755 --- a/scanpy/plotting/_anndata.py +++ b/scanpy/plotting/_anndata.py @@ -144,7 +144,10 @@ def scatter( # store .uns annotations that were added to the new adata object adata.uns = adata_T.uns return axs - raise ValueError('`x`, `y`, and potential `color` inputs must all ' 'come from either `.obs` or `.var`') + raise ValueError( + '`x`, `y`, and potential `color` inputs must all ' + 'come from either `.obs` or `.var`' + ) def _scatter_obs( @@ -182,29 +185,43 @@ def _scatter_obs( use_raw = _check_use_raw(adata, use_raw) # Process layers - if layers in ['X', None] or (isinstance(layers, str) and layers in adata.layers.keys()): + if layers in ['X', None] or ( + isinstance(layers, str) and layers in adata.layers.keys() + ): layers = (layers, layers, layers) elif isinstance(layers, cabc.Collection) and len(layers) == 3: layers = tuple(layers) for layer in layers: if layer not in adata.layers.keys() and layer not in ['X', None]: - raise ValueError('`layers` should have elements that are ' 'either None or in adata.layers.keys().') + raise ValueError( + '`layers` should have elements that are ' + 'either None or in adata.layers.keys().' + ) else: raise ValueError( - "`layers` should be a string or a collection of strings " f"with length 3, had value '{layers}'" + "`layers` should be a string or a collection of strings " + f"with length 3, had value '{layers}'" ) if use_raw and layers not in [('X', 'X', 'X'), (None, None, None)]: ValueError('`use_raw` must be `False` if layers are used.') if legend_loc not in VALID_LEGENDLOCS: - raise ValueError(f'Invalid `legend_loc`, need to be one of: {VALID_LEGENDLOCS}.') + raise ValueError( + f'Invalid `legend_loc`, need to be one of: {VALID_LEGENDLOCS}.' + ) if components is None: components = '1,2' if '2d' in projection else '1,2,3' if isinstance(components, str): components = components.split(',') components = np.array(components).astype(int) - 1 # color can be a obs column name or a matplotlib color specification - keys = ['grey'] if color is None else [color] if isinstance(color, str) or is_color_like(color) else color + keys = ( + ['grey'] + if color is None + else [color] + if isinstance(color, str) or is_color_like(color) + else color + ) if title is not None and isinstance(title, str): title = [title] highlights = adata.uns['highlights'] if 'highlights' in adata.uns else [] @@ -218,7 +235,9 @@ def _scatter_obs( if basis == 'diffmap': components -= 1 except KeyError: - raise KeyError(f'compute coordinates using visualization tool {basis} first') + raise KeyError( + f'compute coordinates using visualization tool {basis} first' + ) elif x is not None and y is not None: if use_raw: if x in adata.obs.columns: @@ -318,7 +337,9 @@ def _scatter_obs( if legend_loc == 'right margin': right_margin = 0.5 if title is None and keys[0] is not None: - title = [key.replace('_', ' ') if not is_color_like(key) else '' for key in keys] + title = [ + key.replace('_', ' ') if not is_color_like(key) else '' for key in keys + ] axs = scatter_base( Y, @@ -378,10 +399,13 @@ def add_centroid(centroids, name, Y, mask): for name in groups: if name not in set(adata.obs[key].cat.categories): raise ValueError( - f'{name!r} is invalid! specify valid name, ' f'one of {adata.obs[key].cat.categories}' + f'{name!r} is invalid! specify valid name, ' + f'one of {adata.obs[key].cat.categories}' ) else: - iname = np.flatnonzero(adata.obs[key].cat.categories.values == name)[0] + iname = np.flatnonzero( + adata.obs[key].cat.categories.values == name + )[0] mask = scatter_group( axs[ikey], key, @@ -412,7 +436,9 @@ def add_centroid(centroids, name, Y, mask): if legend_fontweight is None: legend_fontweight = 'bold' if legend_fontoutline is not None: - path_effect = [patheffects.withStroke(linewidth=legend_fontoutline, foreground='w')] + path_effect = [ + patheffects.withStroke(linewidth=legend_fontoutline, foreground='w') + ] else: path_effect = None for name, pos in centroids.items(): @@ -454,7 +480,9 @@ def add_centroid(centroids, name, Y, mask): fontsize=legend_fontsize, ) elif legend_loc != 'none': - legend = axs[ikey].legend(frameon=False, loc=legend_loc, fontsize=legend_fontsize) + legend = axs[ikey].legend( + frameon=False, loc=legend_loc, fontsize=legend_fontsize + ) if legend is not None: for handle in legend.legendHandles: handle.set_sizes([300.0]) @@ -518,7 +546,11 @@ def ranking( if log: scores = np.log(scores) if labels is None: - labels = adata.var_names if attr in {'var', 'varm'} else np.arange(scores.shape[0]).astype(str) + labels = ( + adata.var_names + if attr in {'var', 'varm'} + else np.arange(scores.shape[0]).astype(str) + ) if isinstance(labels, str): labels = [labels + str(i + 1) for i in range(scores.shape[0])] if n_panels <= 5: @@ -670,9 +702,14 @@ def violin( ylabel = [ylabel] * (1 if groupby is None else len(keys)) if groupby is None: if len(ylabel) != 1: - raise ValueError(f'Expected number of y-labels to be `1`, found `{len(ylabel)}`.') + raise ValueError( + f'Expected number of y-labels to be `1`, found `{len(ylabel)}`.' + ) elif len(ylabel) != len(keys): - raise ValueError(f'Expected number of y-labels to be `{len(keys)}`, ' f'found `{len(ylabel)}`.') + raise ValueError( + f'Expected number of y-labels to be `{len(keys)}`, ' + f'found `{len(ylabel)}`.' + ) if groupby is not None: obs_df = get.obs_df(adata, keys=[groupby] + keys, layer=layer, use_raw=use_raw) @@ -683,7 +720,9 @@ def violin( f'but is of dtype {adata.obs[groupby].dtype}.' ) _utils.add_colors_for_categorical_sample_annotation(adata, groupby) - kwds['palette'] = dict(zip(obs_df[groupby].cat.categories, adata.uns[f'{groupby}_colors'])) + kwds['palette'] = dict( + zip(obs_df[groupby].cat.categories, adata.uns[f'{groupby}_colors']) + ) else: obs_df = get.obs_df(adata, keys=keys, layer=layer, use_raw=use_raw) if groupby is None: @@ -1015,7 +1054,9 @@ def heatmap( # reorder groupby colors if groupby_colors is not None: - groupby_colors = [groupby_colors[x] for x in dendro_data['categories_idx_ordered']] + groupby_colors = [ + groupby_colors[x] for x in dendro_data['categories_idx_ordered'] + ] if show_gene_labels is None: if len(var_names) <= 50: @@ -1105,10 +1146,14 @@ def heatmap( labels, groupby_cmap, norm, - ) = _plot_categories_as_colorblocks(groupby_ax, obs_tidy, colors=groupby_colors, orientation='left') + ) = _plot_categories_as_colorblocks( + groupby_ax, obs_tidy, colors=groupby_colors, orientation='left' + ) # add lines to main heatmap - line_positions = np.cumsum(obs_tidy.index.value_counts(sort=False))[:-1] - 0.5 + line_positions = ( + np.cumsum(obs_tidy.index.value_counts(sort=False))[:-1] - 0.5 + ) heatmap_ax.hlines( line_positions, -0.5, @@ -1121,7 +1166,9 @@ def heatmap( if dendrogram: dendro_ax = fig.add_subplot(axs[1, 2], sharey=heatmap_ax) - _plot_dendrogram(dendro_ax, adata, groupby, ticks=ticks, dendrogram_key=dendrogram) + _plot_dendrogram( + dendro_ax, adata, groupby, ticks=ticks, dendrogram_key=dendrogram + ) # plot group legends on top of heatmap_ax (if given) if var_group_positions is not None and len(var_group_positions) > 0: @@ -1200,9 +1247,13 @@ def heatmap( labels, groupby_cmap, norm, - ) = _plot_categories_as_colorblocks(groupby_ax, obs_tidy, colors=groupby_colors, orientation='bottom') + ) = _plot_categories_as_colorblocks( + groupby_ax, obs_tidy, colors=groupby_colors, orientation='bottom' + ) # add lines to main heatmap - line_positions = np.cumsum(obs_tidy.index.value_counts(sort=False))[:-1] - 0.5 + line_positions = ( + np.cumsum(obs_tidy.index.value_counts(sort=False))[:-1] - 0.5 + ) heatmap_ax.vlines( line_positions, -0.5, @@ -1228,13 +1279,17 @@ def heatmap( if var_group_positions is not None and len(var_group_positions) > 0: gene_groups_ax = fig.add_subplot(axs[1, 1]) arr = [] - for idx, (label, pos) in enumerate(zip(var_group_labels, var_group_positions)): + for idx, (label, pos) in enumerate( + zip(var_group_labels, var_group_positions) + ): if var_groups_subset_of_groupby: label_code = label2code[label] else: label_code = idx arr += [label_code] * (pos[1] + 1 - pos[0]) - gene_groups_ax.imshow(np.array([arr]).T, aspect='auto', cmap=groupby_cmap, norm=norm) + gene_groups_ax.imshow( + np.array([arr]).T, aspect='auto', cmap=groupby_cmap, norm=norm + ) gene_groups_ax.axis('off') # plot colorbar @@ -1361,7 +1416,9 @@ def tracksplot( ) categories = [categories[x] for x in dendro_data['categories_idx_ordered']] - groupby_colors = [groupby_colors[x] for x in dendro_data['categories_idx_ordered']] + groupby_colors = [ + groupby_colors[x] for x in dendro_data['categories_idx_ordered'] + ] obs_tidy = obs_tidy.sort_index() @@ -1452,7 +1509,9 @@ def tracksplot( # the ax to plot the groupby categories is split to add a small space # between the rest of the plot and the categories - axs2 = gridspec.GridSpecFromSubplotSpec(2, 1, subplot_spec=axs[num_rows - 1, 0], height_ratios=[1, 1]) + axs2 = gridspec.GridSpecFromSubplotSpec( + 2, 1, subplot_spec=axs[num_rows - 1, 0], height_ratios=[1, 1] + ) groupby_ax = fig.add_subplot(axs2[1]) @@ -1483,7 +1542,9 @@ def tracksplot( for idx, pos in enumerate(var_group_positions): arr += [idx] * (pos[1] + 1 - pos[0]) - gene_groups_ax.imshow(np.array([arr]).T, aspect='auto', cmap=groupby_cmap, norm=norm) + gene_groups_ax.imshow( + np.array([arr]).T, aspect='auto', cmap=groupby_cmap, norm=norm + ) gene_groups_ax.axis('off') return_ax_dict = {'track_axes': axs_list, 'groupby_ax': groupby_ax} @@ -1790,7 +1851,10 @@ def _prepare_dataframe( f'Given {group}, is not in observations: {adata.obs_keys()}' + msg ) if group in adata.obs.keys() and group == adata.obs.index.name: - raise ValueError(f'Given group {group} is both and index and a column level, ' 'which is ambiguous.') + raise ValueError( + f'Given group {group} is both and index and a column level, ' + 'which is ambiguous.' + ) if group == adata.obs.index.name: groupby_index = group if groupby_index is not None: @@ -1799,7 +1863,9 @@ def _prepare_dataframe( groupby = groupby.copy() # copy to not modify user passed parameter groupby.remove(groupby_index) keys = list(groupby) + list(np.unique(var_names)) - obs_tidy = get.obs_df(adata, keys=keys, layer=layer, use_raw=use_raw, gene_symbols=gene_symbols) + obs_tidy = get.obs_df( + adata, keys=keys, layer=layer, use_raw=use_raw, gene_symbols=gene_symbols + ) assert np.all(np.array(keys) == np.array(obs_tidy.columns)) if groupby_index is not None: @@ -1972,7 +2038,9 @@ def _plot_gene_groups_brackets( # remove y ticks gene_groups_ax.tick_params(axis='y', left=False, labelleft=False) # remove x ticks and labels - gene_groups_ax.tick_params(axis='x', bottom=False, labelbottom=False, labeltop=False) + gene_groups_ax.tick_params( + axis='x', bottom=False, labelbottom=False, labeltop=False + ) def _reorder_categories_after_dendrogram( @@ -2048,7 +2116,9 @@ def _reorder_categories_after_dendrogram( position = var_group_positions[idx] _var_names = var_names[position[0] : position[1] + 1] var_names_idx_ordered.extend(range(position[0], position[1] + 1)) - positions_ordered.append((position_start, position_start + len(_var_names) - 1)) + positions_ordered.append( + (position_start, position_start + len(_var_names) - 1) + ) position_start += len(_var_names) labels_ordered.append(var_group_labels[idx]) var_group_labels = labels_ordered @@ -2106,7 +2176,8 @@ def _get_dendrogram_key(adata, dendrogram_key, groupby): if 'dendrogram_info' not in adata.uns[dendrogram_key]: raise ValueError( - f"The given dendrogram key ({dendrogram_key!r}) does not contain " "valid dendrogram information." + f"The given dendrogram key ({dendrogram_key!r}) does not contain " + "valid dendrogram information." ) return dendrogram_key @@ -2178,7 +2249,9 @@ def translate_pos(pos_list, new_ticks, old_ticks): old_max = old_ticks[idx_next] new_min = new_ticks[idx_prev] new_max = new_ticks[idx_next] - new_x_val = ((x_val - old_min) / (old_max - old_min)) * (new_max - new_min) + new_min + new_x_val = ((x_val - old_min) / (old_max - old_min)) * ( + new_max - new_min + ) + new_min new_xs.append(new_x_val) return new_xs @@ -2190,7 +2263,10 @@ def translate_pos(pos_list, new_ticks, old_ticks): orig_ticks = np.arange(5, len(leaves) * 10 + 5, 10).astype(float) # check that ticks has the same length as orig_ticks if ticks is not None and len(orig_ticks) != len(ticks): - logg.warning("ticks argument does not have the same size as orig_ticks. " "The argument will be ignored") + logg.warning( + "ticks argument does not have the same size as orig_ticks. " + "The argument will be ignored" + ) ticks = None for xs, ys in zip(icoord, dcoord): @@ -2220,7 +2296,9 @@ def translate_pos(pos_list, new_ticks, old_ticks): dendro_ax.tick_params(labeltop=True, labelbottom=False) if remove_labels: - dendro_ax.tick_params(labelbottom=False, labeltop=False, labelleft=False, labelright=False) + dendro_ax.tick_params( + labelbottom=False, labeltop=False, labelleft=False, labelright=False + ) dendro_ax.grid(False) @@ -2271,7 +2349,9 @@ def _plot_categories_as_colorblocks( ticks = [] # list of centered position of the labels labels = [] label2code = {} # dictionary of numerical values asigned to each label - for code, (label, value) in enumerate(obs_tidy.index.value_counts(sort=False).iteritems()): + for code, (label, value) in enumerate( + obs_tidy.index.value_counts(sort=False).iteritems() + ): ticks.append(value_sum + (value / 2)) labels.append(label) value_sum += value diff --git a/scanpy/plotting/_baseplot_class.py b/scanpy/plotting/_baseplot_class.py index 2748348807..5c98ca2694 100644 --- a/scanpy/plotting/_baseplot_class.py +++ b/scanpy/plotting/_baseplot_class.py @@ -100,7 +100,11 @@ def __init__( self.var_group_rotation = var_group_rotation self.width, self.height = figsize if figsize is not None else (None, None) - self.has_var_groups = True if var_group_positions is not None and len(var_group_positions) > 0 else False + self.has_var_groups = ( + True + if var_group_positions is not None and len(var_group_positions) > 0 + else False + ) self._update_var_groups() @@ -115,7 +119,10 @@ def __init__( gene_symbols=gene_symbols, ) if len(self.categories) > self.MAX_NUM_CATEGORIES: - warn(f"Over {self.MAX_NUM_CATEGORIES} categories found. " "Plot would be very large.") + warn( + f"Over {self.MAX_NUM_CATEGORIES} categories found. " + "Plot would be very large." + ) if categories_order is not None: if set(self.obs_tidy.index.categories) != set(categories_order): @@ -249,7 +256,10 @@ def add_dendrogram( if self.groupby is None or len(self.categories) <= 2: # dendrogram can only be computed between groupby categories - logg.warning("Dendrogram not added. Dendrogram is added only " "when the number of categories to plot > 2") + logg.warning( + "Dendrogram not added. Dendrogram is added only " + "when the number of categories to plot > 2" + ) return self self.group_extra_size = size @@ -400,7 +410,9 @@ def get_axes(self): self.make_figure() return self.ax_dict - def _plot_totals(self, total_barplot_ax: Axes, orientation: Literal['top', 'right']): + def _plot_totals( + self, total_barplot_ax: Axes, orientation: Literal['top', 'right'] + ): """ Makes the bar plot for totals """ @@ -495,7 +507,9 @@ def _plot_colorbar(self, color_legend_ax: Axes, normalize): cmap = pl.get_cmap(self.cmap) import matplotlib.colorbar - matplotlib.colorbar.ColorbarBase(color_legend_ax, orientation='horizontal', cmap=cmap, norm=normalize) + matplotlib.colorbar.ColorbarBase( + color_legend_ax, orientation='horizontal', cmap=cmap, norm=normalize + ) color_legend_ax.set_title(self.color_legend_title, fontsize='small') @@ -514,7 +528,9 @@ def _plot_legend(self, legend_ax, return_ax_dict, normalize): self.height - legend_height, legend_height, ] - fig, legend_gs = make_grid_spec(legend_ax, nrows=2, ncols=1, height_ratios=height_ratios) + fig, legend_gs = make_grid_spec( + legend_ax, nrows=2, ncols=1, height_ratios=height_ratios + ) color_legend_ax = fig.add_subplot(legend_gs[1]) @@ -554,12 +570,20 @@ def _mainplot(self, ax): ax.set_ylim(len(y_labels), 0) ax.set_xlim(0, len(x_labels)) +<<<<<<< HEAD return check_colornorm( self.vboundnorm.vmin, self.vboundnorm.vmax, self.vboundnorm.vcenter, self.vboundnorm.norm, ) +======= + normalize = matplotlib.colors.Normalize( + vmin=self.kwds.get('vmin'), vmax=self.kwds.get('vmax') + ) + + return normalize +>>>>>>> 617168f7 (address review) def make_figure(self): """ @@ -588,7 +612,9 @@ def make_figure(self): if self.height is None: mainplot_height = len(self.categories) * category_height - mainplot_width = len(self.var_names) * category_width + self.group_extra_size + mainplot_width = ( + len(self.var_names) * category_width + self.group_extra_size + ) if self.are_axes_swapped: mainplot_height, mainplot_width = mainplot_width, mainplot_height @@ -853,7 +879,9 @@ def _format_first_three_categories(_categories): position = self.var_group_positions[idx] _var_names = self.var_names[position[0] : position[1] + 1] var_names_idx_ordered.extend(range(position[0], position[1] + 1)) - positions_ordered.append((position_start, position_start + len(_var_names) - 1)) + positions_ordered.append( + (position_start, position_start + len(_var_names) - 1) + ) position_start += len(_var_names) labels_ordered.append(self.var_group_labels[idx]) self.var_group_labels = labels_ordered @@ -999,7 +1027,9 @@ def _plot_var_groups_brackets( # remove y ticks gene_groups_ax.tick_params(axis='y', left=False, labelleft=False) # remove x ticks and labels - gene_groups_ax.tick_params(axis='x', bottom=False, labelbottom=False, labeltop=False) + gene_groups_ax.tick_params( + axis='x', bottom=False, labelbottom=False, labeltop=False + ) def _update_var_groups(self): """ diff --git a/scanpy/plotting/_dotplot.py b/scanpy/plotting/_dotplot.py index 3a9856c308..5eeccc8345 100644 --- a/scanpy/plotting/_dotplot.py +++ b/scanpy/plotting/_dotplot.py @@ -165,12 +165,16 @@ def __init__( # of values >expression_cutoff, and divide the result by the total number of # values in the group (given by `count()`) if dot_size_df is None: - dot_size_df = obs_bool.groupby(level=0).sum() / obs_bool.groupby(level=0).count() + dot_size_df = ( + obs_bool.groupby(level=0).sum() / obs_bool.groupby(level=0).count() + ) if dot_color_df is None: # 2. compute mean expression value value if mean_only_expressed: - dot_color_df = self.obs_tidy.mask(~obs_bool).groupby(level=0).mean().fillna(0) + dot_color_df = ( + self.obs_tidy.mask(~obs_bool).groupby(level=0).mean().fillna(0) + ) else: dot_color_df = self.obs_tidy.groupby(level=0).mean() @@ -201,7 +205,9 @@ def __init__( # with df[['a', 'a', 'b']], results in a df with columns: # ['a', 'a', 'a', 'a', 'b'] - unique_var_names, unique_idx = np.unique(dot_color_df.columns, return_index=True) + unique_var_names, unique_idx = np.unique( + dot_color_df.columns, return_index=True + ) # remove duplicate columns if len(unique_var_names) != len(self.var_names): dot_color_df = dot_color_df.iloc[:, unique_idx] @@ -441,11 +447,15 @@ def _plot_size_legend(self, size_legend_ax: Axes): zorder=100, ) size_legend_ax.set_xticks(np.arange(len(size)) + 0.5) - labels = ["{}".format(np.round((x * 100), decimals=0).astype(int)) for x in size_range] + labels = [ + "{}".format(np.round((x * 100), decimals=0).astype(int)) for x in size_range + ] size_legend_ax.set_xticklabels(labels, fontsize='small') # remove y ticks and labels - size_legend_ax.tick_params(axis='y', left=False, labelleft=False, labelright=False) + size_legend_ax.tick_params( + axis='y', left=False, labelleft=False, labelright=False + ) # remove surrounding lines size_legend_ax.spines['right'].set_visible(False) @@ -482,7 +492,9 @@ def _plot_legend(self, legend_ax, return_ax_dict, normalize): spacer_height, cbar_legend_height, ] - fig, legend_gs = make_grid_spec(legend_ax, nrows=4, ncols=1, height_ratios=height_ratios) + fig, legend_gs = make_grid_spec( + legend_ax, nrows=4, ncols=1, height_ratios=height_ratios + ) if self.show_size_legend: size_legend_ax = fig.add_subplot(legend_gs[1]) @@ -636,7 +648,8 @@ def _dotplot( ) assert list(dot_size.columns) == list(dot_color.columns), ( - 'please check that the dot_size ' 'and dot_color dataframes have the same columns' + 'please check that the dot_size ' + 'and dot_color dataframes have the same columns' ) if standard_scale == 'group': @@ -686,7 +699,16 @@ def _dotplot( size = frac ** size_exponent # rescale size to match smallest_dot and largest_dot size = size * (largest_dot - smallest_dot) + smallest_dot +<<<<<<< HEAD normalize = check_colornorm(vmin, vmax, vcenter, norm) +======= + + import matplotlib.colors + + normalize = matplotlib.colors.Normalize( + vmin=kwds.get('vmin'), vmax=kwds.get('vmax') + ) +>>>>>>> 617168f7 (address review) if color_on == 'square': if edge_color is None: @@ -737,7 +759,9 @@ def _dotplot( y_ticks = np.arange(dot_color.shape[0]) + 0.5 dot_ax.set_yticks(y_ticks) - dot_ax.set_yticklabels([dot_color.index[idx] for idx, _ in enumerate(y_ticks)], minor=False) + dot_ax.set_yticklabels( + [dot_color.index[idx] for idx, _ in enumerate(y_ticks)], minor=False + ) x_ticks = np.arange(dot_color.shape[1]) + 0.5 dot_ax.set_xticks(x_ticks) diff --git a/scanpy/plotting/_matrixplot.py b/scanpy/plotting/_matrixplot.py index 99a23b2300..ee71b82f0d 100644 --- a/scanpy/plotting/_matrixplot.py +++ b/scanpy/plotting/_matrixplot.py @@ -215,11 +215,19 @@ def _mainplot(self, ax): cmap = pl.get_cmap(self.kwds.get('cmap', self.cmap)) if 'cmap' in self.kwds: del self.kwds['cmap'] +<<<<<<< HEAD normalize = check_colornorm( self.vboundnorm.vmin, self.vboundnorm.vmax, self.vboundnorm.vcenter, self.vboundnorm.norm, +======= + + import matplotlib.colors + + normalize = matplotlib.colors.Normalize( + vmin=self.kwds.get('vmin'), vmax=self.kwds.get('vmax') +>>>>>>> 617168f7 (address review) ) for axis in ['top', 'bottom', 'left', 'right']: diff --git a/scanpy/plotting/_preprocessing.py b/scanpy/plotting/_preprocessing.py index ad9e64d27b..9837773334 100644 --- a/scanpy/plotting/_preprocessing.py +++ b/scanpy/plotting/_preprocessing.py @@ -120,4 +120,6 @@ def filter_genes_dispersion( A string is appended to the default filename. Infer the filetype if ending on {{`'.pdf'`, `'.png'`, `'.svg'`}}. """ - highly_variable_genes(result, log=log, show=show, save=save, highly_variable_genes=False) + highly_variable_genes( + result, log=log, show=show, save=save, highly_variable_genes=False + ) diff --git a/scanpy/plotting/_qc.py b/scanpy/plotting/_qc.py index d33d5fec2a..3259e66425 100644 --- a/scanpy/plotting/_qc.py +++ b/scanpy/plotting/_qc.py @@ -75,8 +75,14 @@ def highest_expr_genes( mean_percent = norm_dict['X'].mean(axis=0) top_idx = np.argsort(mean_percent)[::-1][:n_top] counts_top_genes = norm_dict['X'][:, top_idx] - columns = adata.var_names[top_idx] if gene_symbols is None else adata.var[gene_symbols][top_idx] - counts_top_genes = pd.DataFrame(counts_top_genes, index=adata.obs_names, columns=columns) + columns = ( + adata.var_names[top_idx] + if gene_symbols is None + else adata.var[gene_symbols][top_idx] + ) + counts_top_genes = pd.DataFrame( + counts_top_genes, index=adata.obs_names, columns=columns + ) if not ax: # figsize is hardcoded to produce a tall image. To change the fig size, diff --git a/scanpy/plotting/_stacked_violin.py b/scanpy/plotting/_stacked_violin.py index d291e6e4d9..e364939e8a 100644 --- a/scanpy/plotting/_stacked_violin.py +++ b/scanpy/plotting/_stacked_violin.py @@ -316,7 +316,9 @@ def _mainplot(self, ax): _matrix = _matrix.iloc[:, self.var_names_idx_order] if self.categories_order is not None: - _matrix.index = _matrix.index.reorder_categories(self.categories_order, ordered=True) + _matrix.index = _matrix.index.reorder_categories( + self.categories_order, ordered=True + ) # get mean values for color and transform to color values # using colormap @@ -324,6 +326,12 @@ def _mainplot(self, ax): if self.are_axes_swapped: _color_df = _color_df.T +<<<<<<< HEAD +======= + norm = matplotlib.colors.Normalize( + vmin=self.kwds.get('vmin'), vmax=self.kwds.get('vmax') + ) +>>>>>>> 617168f7 (address review) cmap = pl.get_cmap(self.kwds.get('cmap', self.cmap)) if 'cmap' in self.kwds: del self.kwds['cmap'] @@ -336,7 +344,9 @@ def _mainplot(self, ax): colormap_array = cmap(normalize(_color_df.values)) x_spacer_size = self.plot_x_padding y_spacer_size = self.plot_y_padding - self._make_rows_of_violinplots(ax, _matrix, colormap_array, _color_df, x_spacer_size, y_spacer_size) + self._make_rows_of_violinplots( + ax, _matrix, colormap_array, _color_df, x_spacer_size, y_spacer_size + ) # turn on axis for `ax` as this is turned off # by make_grid_spec when the axis is subdivided earlier. @@ -351,7 +361,9 @@ def _mainplot(self, ax): # 0.5 to position the ticks on the center of the violins y_ticks = np.arange(_color_df.shape[0]) + 0.5 ax.set_yticks(y_ticks) - ax.set_yticklabels([_color_df.index[idx] for idx, _ in enumerate(y_ticks)], minor=False) + ax.set_yticklabels( + [_color_df.index[idx] for idx, _ in enumerate(y_ticks)], minor=False + ) # 0.5 to position the ticks on the center of the violins x_ticks = np.arange(_color_df.shape[1]) + 0.5 @@ -366,7 +378,9 @@ def _mainplot(self, ax): return normalize - def _make_rows_of_violinplots(self, ax, _matrix, colormap_array, _color_df, x_spacer_size, y_spacer_size): + def _make_rows_of_violinplots( + self, ax, _matrix, colormap_array, _color_df, x_spacer_size, y_spacer_size + ): import seaborn as sns # Slow import, only import if called row_palette = self.kwds.get('color', self.row_palette) @@ -405,8 +419,14 @@ def _make_rows_of_violinplots(self, ax, _matrix, colormap_array, _color_df, x_sp } ) ) - df['genes'] = df['genes'].astype('category').cat.reorder_categories(_matrix.columns) - df['categories'] = df['categories'].astype('category').cat.reorder_categories(_matrix.index.categories) + df['genes'] = ( + df['genes'].astype('category').cat.reorder_categories(_matrix.columns) + ) + df['categories'] = ( + df['categories'] + .astype('category') + .cat.reorder_categories(_matrix.index.categories) + ) # the ax need to be subdivided # define a layout of nrows = len(categories) rows @@ -508,7 +528,9 @@ def _setup_violin_axes_ticks(self, row_ax, num_cols): import matplotlib.ticker as ticker # use MaxNLocator to set 2 ticks - row_ax.yaxis.set_major_locator(ticker.MaxNLocator(nbins=2, steps=[1, 1.2, 10])) + row_ax.yaxis.set_major_locator( + ticker.MaxNLocator(nbins=2, steps=[1, 1.2, 10]) + ) yticks = row_ax.get_yticks() row_ax.set_yticks([yticks[0], yticks[-1]]) ticklabels = row_ax.get_yticklabels() @@ -525,7 +547,9 @@ def _setup_violin_axes_ticks(self, row_ax, num_cols): row_ax.set_xlabel('') row_ax.set_xticklabels([]) - row_ax.tick_params(axis='x', bottom=False, top=False, labeltop=False, labelbottom=False) + row_ax.tick_params( + axis='x', bottom=False, top=False, labeltop=False, labelbottom=False + ) @_doc_params( diff --git a/scanpy/plotting/_tools/__init__.py b/scanpy/plotting/_tools/__init__.py index 67bae71b2c..1be61d2a0e 100644 --- a/scanpy/plotting/_tools/__init__.py +++ b/scanpy/plotting/_tools/__init__.py @@ -399,10 +399,14 @@ def _rank_genes_groups_plot( if min_logfoldchange is not None: df = rank_genes_groups_df(adata, group, key=key) # select genes with given log_fold change - genes_list = df[df.logfoldchanges > min_logfoldchange].names.tolist()[:n_genes] + genes_list = df[df.logfoldchanges > min_logfoldchange].names.tolist()[ + :n_genes + ] else: # get all genes that are 'non-nan' - genes_list = [gene for gene in adata.uns[key]['names'][group] if not pd.isnull(gene)][:n_genes] + genes_list = [ + gene for gene in adata.uns[key]['names'][group] if not pd.isnull(gene) + ][:n_genes] if len(genes_list) == 0: logg.warning(f'No genes found for group {group}') @@ -443,7 +447,9 @@ def _rank_genes_groups_plot( elif plot_type == 'matrixplot': from .._matrixplot import matrixplot - _pl = matrixplot(adata, var_names, groupby, values_df=values_df, return_fig=True, **kwds) + _pl = matrixplot( + adata, var_names, groupby, values_df=values_df, return_fig=True, **kwds + ) if title is not None and 'colorbar_title' not in kwds: _pl.legend(title=title) @@ -954,7 +960,11 @@ def rank_genes_groups_violin( _ax.legend_.remove() _ax.set_ylabel('expression') _ax.set_xticklabels(new_gene_names, rotation='vertical') - writekey = f"rank_genes_groups_" f"{adata.uns[key]['params']['groupby']}_" f"{group_name}" + writekey = ( + f"rank_genes_groups_" + f"{adata.uns[key]['params']['groupby']}_" + f"{group_name}" + ) savefig_or_show(writekey, show=show, save=save) axs.append(_ax) if not show: @@ -1147,11 +1157,15 @@ def embedding_density( if f'X_{basis}' not in adata.obsm_keys(): raise ValueError( - f'Cannot find the embedded representation `adata.obsm[X_{basis!r}]`. ' 'Compute the embedding first.' + f'Cannot find the embedded representation `adata.obsm[X_{basis!r}]`. ' + 'Compute the embedding first.' ) if key not in adata.obs or f'{key}_params' not in adata.uns: - raise ValueError('Please run `sc.tl.embedding_density()` first ' 'and specify the correct key.') + raise ValueError( + 'Please run `sc.tl.embedding_density()` first ' + 'and specify the correct key.' + ) if 'components' in kwargs: logg.warning( @@ -1174,11 +1188,15 @@ def embedding_density( if group is None and groupby is not None: raise ValueError( - 'Densities were calculated over an `.obs` covariate. ' 'Please specify a group from this covariate to plot.' + 'Densities were calculated over an `.obs` covariate. ' + 'Please specify a group from this covariate to plot.' ) if group is not None and groupby is None: - logg.warning("value of 'group' is ignored because densities " "were not calculated for an `.obs` covariate.") + logg.warning( + "value of 'group' is ignored because densities " + "were not calculated for an `.obs` covariate." + ) group = None if np.min(adata.obs[key]) < 0 or np.max(adata.obs[key]) > 1: @@ -1205,7 +1223,11 @@ def embedding_density( # if group is set, then plot it using multiple panels # (even if only one group is set) - if group is not None and not isinstance(group, str) and isinstance(group, cabc.Sequence): + if ( + group is not None + and not isinstance(group, str) + and isinstance(group, cabc.Sequence) + ): if ax is not None: raise ValueError("Can only specify `ax` if no `group` sequence is given.") fig, gs = _panel_grid(hspace, wspace, ncols, len(group)) @@ -1365,7 +1387,9 @@ def _get_values_to_plot( column = values_to_plot.replace('log10_', '') else: column = values_to_plot - values_df = pd.pivot(values_df, index='names', columns='group', values=column).fillna(1) + values_df = pd.pivot( + values_df, index='names', columns='group', values=column + ).fillna(1) if values_to_plot in ['log10_pvals', 'log10_pvals_adj']: values_df = -1 * np.log10(values_df) diff --git a/scanpy/plotting/_tools/paga.py b/scanpy/plotting/_tools/paga.py index 56960717d0..4f565b18ba 100644 --- a/scanpy/plotting/_tools/paga.py +++ b/scanpy/plotting/_tools/paga.py @@ -206,19 +206,26 @@ def _compute_pos( iterations = layout_kwds['iterations'] else: iterations = 500 - pos_list = forceatlas2.forceatlas2(adjacency_solid, pos=init_coords, iterations=iterations) + pos_list = forceatlas2.forceatlas2( + adjacency_solid, pos=init_coords, iterations=iterations + ) pos = {n: [p[0], -p[1]] for n, p in enumerate(pos_list)} elif layout == 'eq_tree': nx_g_tree = nx.Graph(adj_tree) pos = _utils.hierarchy_pos(nx_g_tree, root) if len(pos) < adjacency_solid.shape[0]: - raise ValueError('This is a forest and not a single tree. ' 'Try another `layout`, e.g., {\'fr\'}.') + raise ValueError( + 'This is a forest and not a single tree. ' + 'Try another `layout`, e.g., {\'fr\'}.' + ) else: # igraph layouts g = _sc_utils.get_igraph_from_adjacency(adjacency_solid) if 'rt' in layout: g_tree = _sc_utils.get_igraph_from_adjacency(adj_tree) - pos_list = g_tree.layout(layout, root=root if isinstance(root, list) else [root]).coords + pos_list = g_tree.layout( + layout, root=root if isinstance(root, list) else [root] + ).coords elif layout == 'circle': pos_list = g.layout(layout).coords else: @@ -233,7 +240,9 @@ def _compute_pos( init_pos[:, 1] *= -1 init_coords = init_pos.tolist() try: - pos_list = g.layout(layout, seed=init_coords, weights='weight', **layout_kwds).coords + pos_list = g.layout( + layout, seed=init_coords, weights='weight', **layout_kwds + ).coords except AttributeError: # hack for empty graphs... pos_list = g.layout(layout, seed=init_coords, **layout_kwds).coords pos = {n: [p[0], -p[1]] for n, p in enumerate(pos_list)} @@ -432,12 +441,18 @@ def paga( groups_key = adata.uns['paga']['groups'] def is_flat(x): - has_one_per_category = isinstance(x, cabc.Collection) and len(x) == len(adata.obs[groups_key].cat.categories) + has_one_per_category = isinstance(x, cabc.Collection) and len(x) == len( + adata.obs[groups_key].cat.categories + ) return has_one_per_category or x is None or isinstance(x, str) - if isinstance(colors, cabc.Mapping) and isinstance(colors[next(iter(colors))], cabc.Mapping): + if isinstance(colors, cabc.Mapping) and isinstance( + colors[next(iter(colors))], cabc.Mapping + ): # handle paga pie, remap string keys to integers - names_to_ixs = {n: i for i, n in enumerate(adata.obs[groups_key].cat.categories)} + names_to_ixs = { + n: i for i, n in enumerate(adata.obs[groups_key].cat.categories) + } colors = {names_to_ixs.get(n, n): v for n, v in colors.items()} if is_flat(colors): colors = [colors] @@ -458,14 +473,21 @@ def is_flat(x): if colorbar is None: var_names = adata.var_names if adata.raw is None else adata.raw.var_names colorbars = [ - ((c in adata.obs_keys() and adata.obs[c].dtype.name != 'category') or (c in var_names)) for c in colors + ( + (c in adata.obs_keys() and adata.obs[c].dtype.name != 'category') + or (c in var_names) + ) + for c in colors ] else: colorbars = [False for _ in colors] if isinstance(root, str): if root not in labels: - raise ValueError('If `root` is a string, ' f'it needs to be one of {labels} not {root!r}.') + raise ValueError( + 'If `root` is a string, ' + f'it needs to be one of {labels} not {root!r}.' + ) root = list(labels).index(root) if isinstance(root, cabc.Sequence) and root[0] in labels: root = [list(labels).index(r) for r in root] @@ -609,7 +631,11 @@ def _paga_graph( import networkx as nx node_labels = labels # rename for clarity - if node_labels is not None and isinstance(node_labels, str) and node_labels != adata.uns['paga']['groups']: + if ( + node_labels is not None + and isinstance(node_labels, str) + and node_labels != adata.uns['paga']['groups'] + ): raise ValueError( 'Provide a list of group labels for the PAGA groups {}, not {}.'.format( adata.uns['paga']['groups'], node_labels @@ -620,9 +646,9 @@ def _paga_graph( node_labels = adata.obs[groups_key].cat.categories if (colors is None or colors == groups_key) and groups_key is not None: - if groups_key + '_colors' not in adata.uns or len(adata.obs[groups_key].cat.categories) != len( - adata.uns[groups_key + '_colors'] - ): + if groups_key + '_colors' not in adata.uns or len( + adata.obs[groups_key].cat.categories + ) != len(adata.uns[groups_key + '_colors']): _utils.add_colors_for_categorical_sample_annotation(adata, groups_key) colors = adata.uns[groups_key + '_colors'] for iname, name in enumerate(adata.obs[groups_key].cat.categories): @@ -688,7 +714,11 @@ def _paga_graph( colors = x_color # plot continuous annotation - if isinstance(colors, str) and colors in adata.obs and not is_categorical_dtype(adata.obs[colors]): + if ( + isinstance(colors, str) + and colors in adata.obs + and not is_categorical_dtype(adata.obs[colors]) + ): x_color = [] cats = adata.obs[groups_key].cat.categories for icat, cat in enumerate(cats): @@ -697,7 +727,11 @@ def _paga_graph( colors = x_color # plot categorical annotation - if isinstance(colors, str) and colors in adata.obs and is_categorical_dtype(adata.obs[colors]): + if ( + isinstance(colors, str) + and colors in adata.obs + and is_categorical_dtype(adata.obs[colors]) + ): asso_names, asso_matrix = _sc_utils.compute_association_matrix_of_groups( adata, prediction=groups_key, @@ -705,11 +739,16 @@ def _paga_graph( normalization='reference' if normalize_to_color else 'prediction', ) _utils.add_colors_for_categorical_sample_annotation(adata, colors) - asso_colors = _sc_utils.get_associated_colors_of_groups(adata.uns[colors + '_colors'], asso_matrix) + asso_colors = _sc_utils.get_associated_colors_of_groups( + adata.uns[colors + '_colors'], asso_matrix + ) colors = asso_colors if len(colors) != len(node_labels): - raise ValueError(f'Expected `colors` to be of length `{len(node_labels)}`, ' f'found `{len(colors)}`.') + raise ValueError( + f'Expected `colors` to be of length `{len(node_labels)}`, ' + f'found `{len(colors)}`.' + ) # count number of connected components n_components, labels = scipy.sparse.csgraph.connected_components(adjacency_solid) @@ -725,8 +764,13 @@ def _paga_graph( adjacency_solid = adjacency_solid.tocsc()[:, labels == largest_component] colors = np.array(colors)[labels == largest_component] node_labels = np.array(node_labels)[labels == largest_component] - cats_dropped = adata.obs[groups_key].cat.categories[labels != largest_component].tolist() - logg.info('Restricting graph to largest connected component by dropping categories\n' f'{cats_dropped}') + cats_dropped = ( + adata.obs[groups_key].cat.categories[labels != largest_component].tolist() + ) + logg.info( + 'Restricting graph to largest connected component by dropping categories\n' + f'{cats_dropped}' + ) nx_g_solid = nx.Graph(adjacency_solid) if dashed_edges is not None: raise ValueError('`single_component` only if `dashed_edges` is `None`.') @@ -758,7 +802,9 @@ def _paga_graph( widths = np.clip(widths, min_edge_width, max_edge_width) with warnings.catch_warnings(): warnings.simplefilter("ignore") - nx.draw_networkx_edges(nx_g_solid, pos, ax=ax, width=widths, edge_color='black') + nx.draw_networkx_edges( + nx_g_solid, pos, ax=ax, width=widths, edge_color='black' + ) # draw directed edges else: adjacency_transitions = adata.uns['paga'][transitions].copy() @@ -771,7 +817,9 @@ def _paga_graph( widths = base_edge_width * np.array(widths) if min_edge_width is not None or max_edge_width is not None: widths = np.clip(widths, min_edge_width, max_edge_width) - nx.draw_networkx_edges(g_dir, pos, ax=ax, width=widths, edge_color='black', arrowsize=arrowsize) + nx.draw_networkx_edges( + g_dir, pos, ax=ax, width=widths, edge_color='black', arrowsize=arrowsize + ) if export_to_gexf: if isinstance(colors[0], tuple): @@ -803,15 +851,21 @@ def _paga_graph( else: groups_sizes = np.ones(len(node_labels)) base_scale_scatter = 2000 - base_pie_size = base_scale_scatter / (np.sqrt(adjacency_solid.shape[0]) + 10) * node_size_scale + base_pie_size = ( + base_scale_scatter / (np.sqrt(adjacency_solid.shape[0]) + 10) * node_size_scale + ) median_group_size = np.median(groups_sizes) - groups_sizes = base_pie_size * np.power(groups_sizes / median_group_size, node_size_power) + groups_sizes = base_pie_size * np.power( + groups_sizes / median_group_size, node_size_power + ) if fontsize is None: fontsize = rcParams['legend.fontsize'] if fontoutline is not None: text_kwds = dict(text_kwds) - text_kwds['path_effects'] = [patheffects.withStroke(linewidth=fontoutline, foreground='w')] + text_kwds['path_effects'] = [ + patheffects.withStroke(linewidth=fontoutline, foreground='w') + ] # usual scatter plot if not isinstance(colors[0], cabc.Mapping): n_groups = len(pos_array) @@ -839,7 +893,8 @@ def _paga_graph( for ix, (xx, yy) in enumerate(zip(pos_array[:, 0], pos_array[:, 1])): if not isinstance(colors[ix], cabc.Mapping): raise ValueError( - f'{colors[ix]} is neither a dict of valid ' 'matplotlib colors nor a valid matplotlib color.' + f'{colors[ix]} is neither a dict of valid ' + 'matplotlib colors nor a valid matplotlib color.' ) color_single = colors[ix].keys() fracs = [colors[ix][c] for c in color_single] @@ -850,7 +905,10 @@ def _paga_graph( color_single.append('grey') fracs.append(1 - sum(fracs)) elif not np.isclose(total, 1): - raise ValueError(f'Expected fractions for node `{ix}` to be ' f'close to 1, found `{total}`.') + raise ValueError( + f'Expected fractions for node `{ix}` to be ' + f'close to 1, found `{total}`.' + ) cumsum = np.cumsum(fracs) cumsum = cumsum / cumsum[-1] @@ -864,7 +922,9 @@ def _paga_graph( xy = np.column_stack([x, y]) s = np.abs(xy).max() - sct = ax.scatter([xx], [yy], marker=xy, s=s ** 2 * groups_sizes[ix], color=color) + sct = ax.scatter( + [xx], [yy], marker=xy, s=s ** 2 * groups_sizes[ix], color=color + ) if node_labels is not None: ax.text( @@ -888,7 +948,9 @@ def paga_path( use_raw: bool = True, annotations: Sequence[str] = ('dpt_pseudotime',), color_map: Union[str, Colormap, None] = None, - color_maps_annotations: Mapping[str, Union[str, Colormap]] = MappingProxyType(dict(dpt_pseudotime='Greys')), + color_maps_annotations: Mapping[str, Union[str, Colormap]] = MappingProxyType( + dict(dpt_pseudotime='Greys') + ), palette_groups: Optional[Sequence[str]] = None, n_avg: int = 1, groups_key: Optional[str] = None, @@ -971,13 +1033,17 @@ def paga_path( if groups_key is None: if 'groups' not in adata.uns['paga']: - raise KeyError('Pass the key of the grouping with which you ran PAGA, ' 'using the parameter `groups_key`.') + raise KeyError( + 'Pass the key of the grouping with which you ran PAGA, ' + 'using the parameter `groups_key`.' + ) groups_key = adata.uns['paga']['groups'] groups_names = adata.obs[groups_key].cat.categories if 'dpt_pseudotime' not in adata.obs.keys(): raise ValueError( - '`pl.paga_path` requires computation of a pseudotime `tl.dpt` ' 'for ordering at single-cell resolution' + '`pl.paga_path` requires computation of a pseudotime `tl.dpt` ' + 'for ordering at single-cell resolution' ) if palette_groups is None: @@ -1016,7 +1082,9 @@ def moving_average(a): for ikey, key in enumerate(keys): x = [] for igroup, group in enumerate(nodes_ints): - idcs = np.arange(adata.n_obs)[adata.obs[groups_key].values == nodes_strs[igroup]] + idcs = np.arange(adata.n_obs)[ + adata.obs[groups_key].values == nodes_strs[igroup] + ] if len(idcs) == 0: raise ValueError( 'Did not find data points that match ' @@ -1025,7 +1093,9 @@ def moving_average(a): 'actually contains what you expect.' ) idcs_group = np.argsort( - adata.obs['dpt_pseudotime'].values[adata.obs[groups_key].values == nodes_strs[igroup]] + adata.obs['dpt_pseudotime'].values[ + adata.obs[groups_key].values == nodes_strs[igroup] + ] ) idcs = idcs[idcs_group] values = ( @@ -1151,7 +1221,9 @@ def moving_average(a): ) arr = np.array(anno_dict[anno])[None, :] if anno not in color_maps_annotations: - color_map_anno = 'Vega10' if is_categorical_dtype(adata.obs[anno]) else 'Greys' + color_map_anno = ( + 'Vega10' if is_categorical_dtype(adata.obs[anno]) else 'Greys' + ) else: color_map_anno = color_maps_annotations[anno] img = anno_axis.imshow( diff --git a/scanpy/plotting/_tools/scatterplots.py b/scanpy/plotting/_tools/scatterplots.py index c60946698e..154f638f05 100644 --- a/scanpy/plotting/_tools/scatterplots.py +++ b/scanpy/plotting/_tools/scatterplots.py @@ -145,7 +145,8 @@ def embedding( use_raw = layer is None and adata.raw is not None if use_raw and layer is not None: raise ValueError( - "Cannot use both a layer and the raw representation. Was passed:" f"use_raw={use_raw}, layer={layer}." + "Cannot use both a layer and the raw representation. Was passed:" + f"use_raw={use_raw}, layer={layer}." ) if wspace is None: @@ -153,7 +154,10 @@ def embedding( # current figure size wspace = 0.75 / rcParams['figure.figsize'][0] + 0.02 if adata.raw is None and use_raw: - raise ValueError("`use_raw` is set to True but AnnData object does not have raw. " "Please check.") + raise ValueError( + "`use_raw` is set to True but AnnData object does not have raw. " + "Please check." + ) # turn color into a python list color = [color] if isinstance(color, str) or color is None else list(color) if title is not None: @@ -162,17 +166,24 @@ def embedding( # get the points position and the components list # (only if components is not None) - data_points, components_list = _get_data_points(adata, basis, projection, components, scale_factor) + data_points, components_list = _get_data_points( + adata, basis, projection, components, scale_factor + ) # Setup layout. # Most of the code is for the case when multiple plots are required # 'color' is a list of names that want to be plotted. # Eg. ['Gene1', 'louvain', 'Gene2']. # component_list is a list of components [[0,1], [1,2]] - if (not isinstance(color, str) and isinstance(color, cabc.Sequence) and len(color) > 1) or len(components_list) > 1: + if ( + not isinstance(color, str) + and isinstance(color, cabc.Sequence) + and len(color) > 1 + ) or len(components_list) > 1: if ax is not None: raise ValueError( - "Cannot specify `ax` when plotting multiple panels " "(each for a given value of 'color')." + "Cannot specify `ax` when plotting multiple panels " + "(each for a given value of 'color')." ) if len(components_list) == 0: components_list = [None] @@ -227,7 +238,9 @@ def embedding( # color=gene1, components=[1,2], color=gene1, components=[2,3], # color=gene2, components = [1, 2], color=gene2, components=[2,3], # ] - for count, (value_to_plot, component_idx) in enumerate(itertools.product(color, idx_components)): + for count, (value_to_plot, component_idx) in enumerate( + itertools.product(color, idx_components) + ): color_source_vector = _get_color_source_vector( adata, value_to_plot, @@ -299,8 +312,14 @@ def embedding( if categorical: kwargs['vmin'] = kwargs['vmax'] = None else: +<<<<<<< HEAD kwargs['vmin'], kwargs['vmax'] = _get_vmin_vmax(vmin, vmax, count, color_vector) >>>>>>> 40dc2c3b (add flake8 pre-commit) +======= + kwargs['vmin'], kwargs['vmax'] = _get_vmin_vmax( + vmin, vmax, count, color_vector + ) +>>>>>>> 617168f7 (address review) # make the scatter plot if projection == '3d': @@ -413,7 +432,9 @@ def embedding( continue if legend_fontoutline is not None: - path_effect = [patheffects.withStroke(linewidth=legend_fontoutline, foreground='w')] + path_effect = [ + patheffects.withStroke(linewidth=legend_fontoutline, foreground='w') + ] else: path_effect = None @@ -571,11 +592,17 @@ def _wraps_plot_scatter(wrapper): wrapper_params.pop("adata") params.update(wrapper_params) - annotations = {k: v.annotation for k, v in params.items() if v.annotation != inspect.Parameter.empty} + annotations = { + k: v.annotation + for k, v in params.items() + if v.annotation != inspect.Parameter.empty + } if wrapper_sig.return_annotation is not inspect.Signature.empty: annotations["return"] = wrapper_sig.return_annotation - wrapper.__signature__ = inspect.Signature(list(params.values()), return_annotation=wrapper_sig.return_annotation) + wrapper.__signature__ = inspect.Signature( + list(params.values()), return_annotation=wrapper_sig.return_annotation + ) wrapper.__annotations__ = annotations return wrapper @@ -664,7 +691,9 @@ def diffmap(adata, **kwargs) -> Union[Axes, List[Axes], None]: scatter_bulk=doc_scatter_embedding, show_save_ax=doc_show_save_ax, ) -def draw_graph(adata: AnnData, *, layout: Optional[_IGraphLayout] = None, **kwargs) -> Union[Axes, List[Axes], None]: +def draw_graph( + adata: AnnData, *, layout: Optional[_IGraphLayout] = None, **kwargs +) -> Union[Axes, List[Axes], None]: """\ Scatter plot in graph-drawing basis. @@ -687,7 +716,9 @@ def draw_graph(adata: AnnData, *, layout: Optional[_IGraphLayout] = None, **kwar basis = 'draw_graph_' + layout if 'X_' + basis not in adata.obsm_keys(): raise ValueError( - 'Did not find {} in adata.obs. Did you compute layout {}?'.format('draw_graph_' + layout, layout) + 'Did not find {} in adata.obs. Did you compute layout {}?'.format( + 'draw_graph_' + layout, layout + ) ) return embedding(adata, basis, **kwargs) @@ -725,12 +756,15 @@ def pca( If `show==False` a :class:`~matplotlib.axes.Axes` or a list of it. """ if not annotate_var_explained: - return embedding(adata, 'pca', show=show, return_fig=return_fig, save=save, **kwargs) + return embedding( + adata, 'pca', show=show, return_fig=return_fig, save=save, **kwargs + ) else: if 'pca' not in adata.obsm.keys() and 'X_pca' not in adata.obsm.keys(): raise KeyError( - f"Could not find entry in `obsm` for 'pca'.\n" f"Available keys are: {list(adata.obsm.keys())}." + f"Could not find entry in `obsm` for 'pca'.\n" + f"Available keys are: {list(adata.obsm.keys())}." ) label_dict = { @@ -842,7 +876,9 @@ def spatial( library_id, spatial_data = _check_spatial_data(adata.uns, library_id) img, img_key = _check_img(spatial_data, img, img_key, bw=bw) spot_size = _check_spot_size(spatial_data, spot_size) - scale_factor = _check_scale_factor(spatial_data, img_key=img_key, scale_factor=scale_factor) + scale_factor = _check_scale_factor( + spatial_data, img_key=img_key, scale_factor=scale_factor + ) crop_coord = _check_crop_coord(crop_coord, scale_factor) na_color = _check_na_color(na_color, img=img) @@ -909,7 +945,8 @@ def _get_data_points( basis_key = f"X_{basis}" else: raise KeyError( - f"Could not find entry in `obsm` for '{basis}'.\n" f"Available keys are: {list(adata.obsm.keys())}." + f"Could not find entry in `obsm` for '{basis}'.\n" + f"Available keys are: {list(adata.obsm.keys())}." ) n_dims = 2 @@ -929,7 +966,9 @@ def _get_data_points( r_value = 3 if projection == '3d' else 2 _components_list = np.arange(adata.obsm[basis_key].shape[1]) + 1 - components = [",".join(map(str, x)) for x in combinations(_components_list, r=r_value)] + components = [ + ",".join(map(str, x)) for x in combinations(_components_list, r=r_value) + ] components_list = [] offset = 0 @@ -941,7 +980,9 @@ def _get_data_points( if isinstance(components, str): # eg: components='1,2' - components_list.append(tuple(int(x.strip()) - 1 + offset for x in components.split(','))) + components_list.append( + tuple(int(x.strip()) - 1 + offset for x in components.split(',')) + ) elif isinstance(components, cabc.Sequence): if isinstance(components[0], int): @@ -953,11 +994,14 @@ def _get_data_points( # More than one component can be given and is stored # as a new item of components_list for comp in components: - components_list.append(tuple(int(x.strip()) - 1 + offset for x in comp.split(','))) + components_list.append( + tuple(int(x.strip()) - 1 + offset for x in comp.split(',')) + ) else: raise ValueError( - "Given components: '{}' are not valid. Please check. " "A valid example is `components='2,3'`" + "Given components: '{}' are not valid. Please check. " + "A valid example is `components='2,3'`" ) # check if the components are present in the data try: @@ -966,13 +1010,16 @@ def _get_data_points( data_points.append(adata.obsm[basis_key][:, comp]) except Exception: raise ValueError( - "Given components: '{}' are not valid. Please check. " "A valid example is `components='2,3'`" + "Given components: '{}' are not valid. Please check. " + "A valid example is `components='2,3'`" ) if basis == 'diffmap': # remove the offset added in the case of diffmap, such that # plot_scatter can print the labels correctly. - components_list = [tuple(number - 1 for number in comp) for comp in components_list] + components_list = [ + tuple(number - 1 for number in comp) for comp in components_list + ] else: data_points = [np.array(adata.obsm[basis_key])[:, offset : offset + n_dims]] components_list = [] @@ -999,7 +1046,9 @@ def _add_categorical_legend( """Add a legend to the passed Axes.""" if na_in_legend and pd.isnull(color_source_vector).any(): if "NA" in color_source_vector: - raise NotImplementedError("No fallback for null labels has been defined if NA already in categories.") + raise NotImplementedError( + "No fallback for null labels has been defined if NA already in categories." + ) color_source_vector = color_source_vector.add_categories("NA").fillna("NA") palette = palette.copy() palette["NA"] = na_color @@ -1023,7 +1072,11 @@ def _add_categorical_legend( ) elif legend_loc == 'on data': # identify centroids to put labels - all_pos = pd.DataFrame(scatter_array, columns=["x", "y"]).groupby(color_source_vector, observed=True).median() + all_pos = ( + pd.DataFrame(scatter_array, columns=["x", "y"]) + .groupby(color_source_vector, observed=True) + .median() + ) for label, x_pos, y_pos in all_pos.itertuples(): ax.text( @@ -1041,7 +1094,9 @@ def _add_categorical_legend( _utils._tmp_cluster_pos = all_pos.values -def _get_color_source_vector(adata, value_to_plot, use_raw=False, gene_symbols=None, layer=None, groups=None): +def _get_color_source_vector( + adata, value_to_plot, use_raw=False, gene_symbols=None, layer=None, groups=None +): """ Get array from adata that colors will be based on. """ @@ -1051,7 +1106,11 @@ def _get_color_source_vector(adata, value_to_plot, use_raw=False, gene_symbols=N # _color_vector handles this. # https://github.com/matplotlib/matplotlib/issues/18294 return np.broadcast_to(np.nan, adata.n_obs) - if gene_symbols is not None and value_to_plot not in adata.obs.columns and value_to_plot not in adata.var_names: + if ( + gene_symbols is not None + and value_to_plot not in adata.obs.columns + and value_to_plot not in adata.var_names + ): # We should probably just make an index for this, and share it over runs value_to_plot = adata.var.index[adata.var[gene_symbols] == value_to_plot][ 0 @@ -1070,7 +1129,9 @@ def _get_palette(adata, values_key: str, palette=None): values = pd.Categorical(adata.obs[values_key]) if palette: _utils._set_colors_for_categorical_obs(adata, values_key, palette) - elif color_key not in adata.uns or len(adata.uns[color_key]) < len(values.categories): + elif color_key not in adata.uns or len(adata.uns[color_key]) < len( + values.categories + ): # set a default palette in case that no colors or few colors are found _utils._set_default_colors_for_categorical_obs(adata, values_key) else: @@ -1078,7 +1139,9 @@ def _get_palette(adata, values_key: str, palette=None): return dict(zip(values.categories, adata.uns[color_key])) -def _color_vector(adata, values_key: str, values, palette, na_color="lightgray") -> Tuple[np.ndarray, bool]: +def _color_vector( + adata, values_key: str, values, palette, na_color="lightgray" +) -> Tuple[np.ndarray, bool]: """ Map array of values to array of hex (plus alpha) codes. @@ -1097,7 +1160,10 @@ def _color_vector(adata, values_key: str, values, palette, na_color="lightgray") if not is_categorical_dtype(values): return values, False else: # is_categorical_dtype(values) - color_map = {k: to_hex(v) for k, v in _get_palette(adata, values_key, palette=palette).items()} + color_map = { + k: to_hex(v) + for k, v in _get_palette(adata, values_key, palette=palette).items() + } # If color_map does not have unique values, this can be slow as the # result is not categorical color_vector = values.map(color_map) @@ -1130,14 +1196,19 @@ def _basis2name(basis): return component_name -def _check_spot_size(spatial_data: Optional[Mapping], spot_size: Optional[float]) -> float: +def _check_spot_size( + spatial_data: Optional[Mapping], spot_size: Optional[float] +) -> float: """ Resolve spot_size value. This is a required argument for spatial plots. """ if spatial_data is None and spot_size is None: - raise ValueError("When .uns['spatial'][library_id] does not exist, spot_size must be " "provided directly.") + raise ValueError( + "When .uns['spatial'][library_id] does not exist, spot_size must be " + "provided directly." + ) elif spot_size is None: return spatial_data['scalefactors']['spot_diameter_fullres'] else: @@ -1158,7 +1229,9 @@ def _check_scale_factor( return 1.0 -def _check_spatial_data(uns: Mapping, library_id: Union[Empty, None, str]) -> Tuple[Optional[str], Optional[Mapping]]: +def _check_spatial_data( + uns: Mapping, library_id: Union[Empty, None, str] +) -> Tuple[Optional[str], Optional[Mapping]]: """ Given a mapping, try and extract a library id/ mapping with spatial data. @@ -1215,7 +1288,9 @@ def _check_crop_coord( return crop_coord -def _check_na_color(na_color: Optional[ColorLike], *, img: Optional[np.ndarray] = None) -> ColorLike: +def _check_na_color( + na_color: Optional[ColorLike], *, img: Optional[np.ndarray] = None +) -> ColorLike: if na_color is None: if img is not None: na_color = (0.0, 0.0, 0.0, 0.0) diff --git a/scanpy/plotting/_utils.py b/scanpy/plotting/_utils.py index 71ee0a1622..f99741d3f3 100644 --- a/scanpy/plotting/_utils.py +++ b/scanpy/plotting/_utils.py @@ -31,7 +31,10 @@ _FontSize = Literal[ 'xx-small', 'x-small', 'small', 'medium', 'large', 'x-large', 'xx-large' ] +<<<<<<< HEAD VBound = Union[str, float, Callable[[Sequence[float]], float]] +======= +>>>>>>> 617168f7 (address review) class _AxesSubplot(Axes, axes.SubplotBase, ABC): @@ -70,7 +73,9 @@ def matrix( ax.set_xticks(range(len(xticks)), xticks, rotation='vertical') if yticks is not None: ax.set_yticks(range(len(yticks)), yticks) - pl.colorbar(img, shrink=colorbar_shrink, ax=ax) # need a figure instance for colorbar + pl.colorbar( + img, shrink=colorbar_shrink, ax=ax + ) # need a figure instance for colorbar savefig_or_show('matrix', show=show, save=save) @@ -155,7 +160,9 @@ def timeseries_subplot( ax.legend(frameon=False) -def timeseries_as_heatmap(X: np.ndarray, var_names: Collection[str] = (), highlights_x=(), color_map=None): +def timeseries_as_heatmap( + X: np.ndarray, var_names: Collection[str] = (), highlights_x=(), color_map=None +): """\ Plot timeseries as heatmap. @@ -264,7 +271,10 @@ def savefig(writekey, dpi=None, ext=None): """ if dpi is None: # we need this as in notebooks, the internal figures are also influenced by 'savefig.dpi' this... - if not isinstance(rcParams['savefig.dpi'], str) and rcParams['savefig.dpi'] < 150: + if ( + not isinstance(rcParams['savefig.dpi'], str) + and rcParams['savefig.dpi'] < 150 + ): if settings._low_resolution_warning: logg.warning( 'You are using a low resolution (dpi<150) for saving figures.\n' @@ -352,7 +362,9 @@ def _validate_palette(adata, key): adata.uns[color_key] = _palette -def _set_colors_for_categorical_obs(adata, value_to_plot, palette: Union[str, Sequence[str], Cycler]): +def _set_colors_for_categorical_obs( + adata, value_to_plot, palette: Union[str, Sequence[str], Cycler] +): """ Sets the adata.uns[value_to_plot + '_colors'] according to the given palette @@ -401,7 +413,10 @@ def _set_colors_for_categorical_obs(adata, value_to_plot, palette: Union[str, Se if color in additional_colors: color = additional_colors[color] else: - raise ValueError("The following color value of the given palette " f"is not valid: {color}") + raise ValueError( + "The following color value of the given palette " + f"is not valid: {color}" + ) _color_list.append(color) palette = cycler(color=_color_list) @@ -461,7 +476,9 @@ def _set_default_colors_for_categorical_obs(adata, value_to_plot): adata.uns[value_to_plot + '_colors'] = palette[:length] -def add_colors_for_categorical_sample_annotation(adata, key, palette=None, force_update_colors=False): +def add_colors_for_categorical_sample_annotation( + adata, key, palette=None, force_update_colors=False +): color_key = f"{key}_colors" colors_needed = len(adata.obs[key].cat.categories) @@ -504,10 +521,14 @@ def plot_edges(axs, adata, basis, edges_width, edges_color, neighbors_key=None): def plot_arrows(axs, adata, basis, arrows_kwds=None): if not isinstance(axs, cabc.Sequence): axs = [axs] - v_prefix = next((p for p in ['velocity', 'Delta'] if f'{p}_{basis}' in adata.obsm), None) + v_prefix = next( + (p for p in ['velocity', 'Delta'] if f'{p}_{basis}' in adata.obsm), None + ) if v_prefix is None: raise ValueError( - "`arrows=True` requires " f"`'velocity_{basis}'` from scvelo or " f"`'Delta_{basis}'` from velocyto." + "`arrows=True` requires " + f"`'velocity_{basis}'` from scvelo or " + f"`'Delta_{basis}'` from velocyto." ) if v_prefix == 'velocity': logg.warning( @@ -589,7 +610,9 @@ def setup_axes( if show_ticks: base_width *= 1.1 - draw_region_width = base_width - left_offset - top_offset - 0.5 # this is kept constant throughout + draw_region_width = ( + base_width - left_offset - top_offset - 0.5 + ) # this is kept constant throughout right_margin_factor = sum([1 + right_margin for right_margin in right_margin_list]) width_without_offsets = ( @@ -610,7 +633,9 @@ def setup_axes( left_positions = [left_offset_frac, left_offset_frac + draw_region_width_frac] for i in range(1, len(panels)): right_margin = right_margin_list[i - 1] - left_positions.append(left_positions[-1] + right_margin * draw_region_width_frac) + left_positions.append( + left_positions[-1] + right_margin * draw_region_width_frac + ) left_positions.append(left_positions[-1] + draw_region_width_frac) panel_pos = [[bottom_offset], [1 - top_offset], left_positions] @@ -714,7 +739,10 @@ def scatter_base( ) if colorbars[icolor]: width = 0.006 * draw_region_width / len(colors) - left = panel_pos[2][2 * icolor + 1] + (1.2 if projection == '3d' else 0.2) * width + left = ( + panel_pos[2][2 * icolor + 1] + + (1.2 if projection == '3d' else 0.2) * width + ) rectangle = [left, bottom, width, height] fig = pl.gcf() ax_cb = fig.add_axes(rectangle) @@ -737,7 +765,11 @@ def scatter_base( s=10, zorder=20, ) - highlight_text = highlights_labels[iihighlight] if len(highlights_labels) > 0 else str(ihighlight) + highlight_text = ( + highlights_labels[iihighlight] + if len(highlights_labels) > 0 + else str(ihighlight) + ) # the following is a Python 2 compatibility hack ax.text( *([d[0] for d in data] + [highlight_text]), @@ -752,7 +784,10 @@ def scatter_base( ax.set_zticks([]) # set default axis_labels if axis_labels is None: - axis_labels = [[component_name + str(i) for i in component_indexnames] for _ in range(len(axs))] + axis_labels = [ + [component_name + str(i) for i in component_indexnames] + for _ in range(len(axs)) + ] else: axis_labels = [axis_labels for _ in range(len(axs))] for iax, ax in enumerate(axs): @@ -940,7 +975,9 @@ def zoom(ax, xy='x', factor=1): ---------- """ limits = ax.get_xlim() if xy == 'x' else ax.get_ylim() - new_limits = 0.5 * (limits[0] + limits[1]) + 1.0 / factor * np.array((-0.5, 0.5)) * (limits[1] - limits[0]) + new_limits = 0.5 * (limits[0] + limits[1]) + 1.0 / factor * np.array( + (-0.5, 0.5) + ) * (limits[1] - limits[0]) if xy == 'x': ax.set_xlim(new_limits) else: @@ -1022,7 +1059,9 @@ def check_projection(projection): mpl_version = parse(mpl.__version__) if mpl_version < parse("3.3.3"): - raise ImportError(f"3d plotting requires matplotlib > 3.3.3. Found {mpl.__version__}") + raise ImportError( + f"3d plotting requires matplotlib > 3.3.3. Found {mpl.__version__}" + ) def circles(x, y, s, ax, marker=None, c='b', vmin=None, vmax=None, **kwargs): diff --git a/scanpy/plotting/palettes.py b/scanpy/plotting/palettes.py index f173719049..3ea963de99 100644 --- a/scanpy/plotting/palettes.py +++ b/scanpy/plotting/palettes.py @@ -210,4 +210,6 @@ def _plot_color_cycle(clists: Mapping[str, Sequence[str]]): if __name__ == '__main__': - _plot_color_cycle({name: colors for name, colors in globals().items() if isinstance(colors, list)}) + _plot_color_cycle( + {name: colors for name, colors in globals().items() if isinstance(colors, list)} + ) diff --git a/scanpy/preprocessing/_combat.py b/scanpy/preprocessing/_combat.py index ee5761798c..e2d8140bca 100644 --- a/scanpy/preprocessing/_combat.py +++ b/scanpy/preprocessing/_combat.py @@ -10,7 +10,9 @@ from .._utils import sanitize_anndata -def _design_matrix(model: pd.DataFrame, batch_key: str, batch_levels: Collection[str]) -> pd.DataFrame: +def _design_matrix( + model: pd.DataFrame, batch_key: str, batch_levels: Collection[str] +) -> pd.DataFrame: """\ Computes a simple design matrix. @@ -42,7 +44,9 @@ def _design_matrix(model: pd.DataFrame, batch_key: str, batch_levels: Collection if other_cols: col_repr = " + ".join("Q('{}')".format(x) for x in other_cols) - factor_matrix = patsy.dmatrix("~ 0 + {}".format(col_repr), model[other_cols], return_type="dataframe") + factor_matrix = patsy.dmatrix( + "~ 0 + {}".format(col_repr), model[other_cols], return_type="dataframe" + ) design = pd.concat((design, factor_matrix), axis=1) logg.info(f"Found {len(other_cols)} categorical variables:") @@ -105,7 +109,9 @@ def _standardize_data( # Compute the means if np.sum(var_pooled == 0) > 0: print(f'Found {np.sum(var_pooled == 0)} genes with zero variance.') - stand_mean = np.dot(grand_mean.T.reshape((len(grand_mean), 1)), np.ones((1, int(n_array)))) + stand_mean = np.dot( + grand_mean.T.reshape((len(grand_mean), 1)), np.ones((1, int(n_array))) + ) tmp = np.array(design.copy()) tmp[:, :n_batch] = 0 stand_mean += np.dot(tmp, B_hat).T @@ -169,7 +175,9 @@ def combat( cov_exist = np.isin(covariates, adata.obs_keys()) if np.any(~cov_exist): missing_cov = np.array(covariates)[~cov_exist].tolist() - raise ValueError('Could not find the covariate(s) {!r} in adata.obs'.format(missing_cov)) + raise ValueError( + 'Could not find the covariate(s) {!r} in adata.obs'.format(missing_cov) + ) if key in covariates: raise ValueError('Batch key and covariates cannot overlap') @@ -201,7 +209,9 @@ def combat( logg.info("Fitting L/S model and finding priors\n") batch_design = design[design.columns[:n_batch]] # first estimate of the additive batch effect - gamma_hat = (la.inv(batch_design.T @ batch_design) @ batch_design.T @ s_data.T).values + gamma_hat = ( + la.inv(batch_design.T @ batch_design) @ batch_design.T @ s_data.T + ).values delta_hat = [] # first estimate for the multiplicative batch effect @@ -250,7 +260,10 @@ def combat( dsq = np.sqrt(delta_star[j, :]) dsq = dsq.reshape((len(dsq), 1)) denom = np.dot(dsq, np.ones((1, n_batches[j]))) - numer = np.array(bayesdata.iloc[:, batch_idxs] - np.dot(batch_design.iloc[batch_idxs], gamma_star).T) + numer = np.array( + bayesdata.iloc[:, batch_idxs] + - np.dot(batch_design.iloc[batch_idxs], gamma_star).T + ) bayesdata.iloc[:, batch_idxs] = numer / denom vpsq = np.sqrt(var_pooled).reshape((len(var_pooled), 1)) @@ -316,12 +329,16 @@ def _it_sol( # in the loop, gamma and delta are updated together. they depend on each other. we iterate until convergence. while change > conv: g_new = (t2 * n * g_hat + d_old * g_bar) / (t2 * n + d_old) - sum2 = s_data - g_new.reshape((g_new.shape[0], 1)) @ np.ones((1, s_data.shape[1])) + sum2 = s_data - g_new.reshape((g_new.shape[0], 1)) @ np.ones( + (1, s_data.shape[1]) + ) sum2 = sum2 ** 2 sum2 = sum2.sum(axis=1) d_new = (0.5 * sum2 + b) / (n / 2.0 + a - 1.0) - change = max((abs(g_new - g_old) / g_old).max(), (abs(d_new - d_old) / d_old).max()) + change = max( + (abs(g_new - g_old) / g_old).max(), (abs(d_new - d_old) / d_old).max() + ) g_old = g_new # .copy() d_old = d_new # .copy() count = count + 1 diff --git a/scanpy/preprocessing/_deprecated/__init__.py b/scanpy/preprocessing/_deprecated/__init__.py index 0e768454c1..2bb8730540 100644 --- a/scanpy/preprocessing/_deprecated/__init__.py +++ b/scanpy/preprocessing/_deprecated/__init__.py @@ -36,9 +36,15 @@ def normalize_per_cell_weinreb16_deprecated( gene_subset = np.all(X <= counts_per_cell[:, None] * max_fraction, axis=0) if issparse(X): gene_subset = gene_subset.A1 - tc_include = X[:, gene_subset].sum(1).A1 if issparse(X) else X[:, gene_subset].sum(1) + tc_include = ( + X[:, gene_subset].sum(1).A1 if issparse(X) else X[:, gene_subset].sum(1) + ) - X_norm = X.multiply(csr_matrix(1 / tc_include[:, None])) if issparse(X) else X / tc_include[:, None] + X_norm = ( + X.multiply(csr_matrix(1 / tc_include[:, None])) + if issparse(X) + else X / tc_include[:, None] + ) if mult_with_mean: X_norm *= np.mean(counts_per_cell) diff --git a/scanpy/preprocessing/_deprecated/highly_variable_genes.py b/scanpy/preprocessing/_deprecated/highly_variable_genes.py index 86d68c3a77..1487b60feb 100644 --- a/scanpy/preprocessing/_deprecated/highly_variable_genes.py +++ b/scanpy/preprocessing/_deprecated/highly_variable_genes.py @@ -104,7 +104,9 @@ def filter_genes_dispersion( If a data matrix `X` is passed, the annotation is returned as `np.recarray` with the same information stored in fields: `gene_subset`, `means`, `dispersions`, `dispersion_norm`. """ - if n_top_genes is not None and not all(x is None for x in [min_disp, max_disp, min_mean, max_mean]): + if n_top_genes is not None and not all( + x is None for x in [min_disp, max_disp, min_mean, max_mean] + ): logg.info('If you pass `n_top_genes`, all cutoffs are ignored.') if min_disp is None: min_disp = 0.5 @@ -168,7 +170,10 @@ def filter_genes_dispersion( disp_mean_bin[one_gene_per_bin] = 0 # actually do the normalization df['dispersion_norm'] = ( - df['dispersion'].values - disp_mean_bin[df['mean_bin'].values].values # use values here as index differs + df['dispersion'].values + - disp_mean_bin[ + df['mean_bin'].values + ].values # use values here as index differs ) / disp_std_bin[df['mean_bin'].values].values elif flavor == 'cell_ranger': from statsmodels import robust @@ -184,7 +189,9 @@ def filter_genes_dispersion( warnings.simplefilter('ignore') disp_mad_bin = disp_grouped.apply(robust.mad) df['dispersion_norm'] = ( - np.abs(df['dispersion'].values - disp_median_bin[df['mean_bin'].values].values) + np.abs( + df['dispersion'].values - disp_median_bin[df['mean_bin'].values].values + ) / disp_mad_bin[df['mean_bin'].values].values ) else: @@ -192,10 +199,15 @@ def filter_genes_dispersion( dispersion_norm = df['dispersion_norm'].values.astype('float32') if n_top_genes is not None: dispersion_norm = dispersion_norm[~np.isnan(dispersion_norm)] - dispersion_norm[::-1].sort() # interestingly, np.argpartition is slightly slower + dispersion_norm[ + ::-1 + ].sort() # interestingly, np.argpartition is slightly slower disp_cut_off = dispersion_norm[n_top_genes - 1] gene_subset = df['dispersion_norm'].values >= disp_cut_off - logg.debug(f'the {n_top_genes} top genes correspond to a ' f'normalized dispersion cutoff of {disp_cut_off}') + logg.debug( + f'the {n_top_genes} top genes correspond to a ' + f'normalized dispersion cutoff of {disp_cut_off}' + ) else: max_disp = np.inf if max_disp is None else max_disp dispersion_norm[np.isnan(dispersion_norm)] = 0 # similar to Seurat diff --git a/scanpy/preprocessing/_highly_variable_genes.py b/scanpy/preprocessing/_highly_variable_genes.py index 591a0d97cd..9eb224fe4d 100644 --- a/scanpy/preprocessing/_highly_variable_genes.py +++ b/scanpy/preprocessing/_highly_variable_genes.py @@ -52,7 +52,9 @@ def _highly_variable_genes_seurat_v3( try: from skmisc.loess import loess except ImportError: - raise ImportError('Please install skmisc package via `pip install --user scikit-misc') + raise ImportError( + 'Please install skmisc package via `pip install --user scikit-misc' + ) X = adata.layers[layer] if layer is not None else adata.X <<<<<<< HEAD @@ -113,7 +115,9 @@ def _highly_variable_genes_seurat_v3( batch_counts_sum = batch_counts.sum(axis=0) norm_gene_var = (1 / ((N - 1) * np.square(reg_std))) * ( - (N * np.square(mean)) + squared_batch_counts_sum - 2 * batch_counts_sum * mean + (N * np.square(mean)) + + squared_batch_counts_sum + - 2 * batch_counts_sum * mean ) norm_gene_vars.append(norm_gene_var.reshape(1, -1)) @@ -123,7 +127,9 @@ def _highly_variable_genes_seurat_v3( # this is done in SelectIntegrationFeatures() in Seurat v3 ranked_norm_gene_vars = ranked_norm_gene_vars.astype(np.float32) - num_batches_high_var = np.sum((ranked_norm_gene_vars < n_top_genes).astype(int), axis=0) + num_batches_high_var = np.sum( + (ranked_norm_gene_vars < n_top_genes).astype(int), axis=0 + ) ranked_norm_gene_vars[ranked_norm_gene_vars >= n_top_genes] = np.nan ma_ranked = np.ma.masked_invalid(ranked_norm_gene_vars) median_ranked = np.ma.median(ma_ranked, axis=0).filled(np.nan) @@ -159,9 +165,13 @@ def _highly_variable_genes_seurat_v3( adata.var['highly_variable_rank'] = df['highly_variable_rank'].values adata.var['means'] = df['means'].values adata.var['variances'] = df['variances'].values - adata.var['variances_norm'] = df['variances_norm'].values.astype('float64', copy=False) + adata.var['variances_norm'] = df['variances_norm'].values.astype( + 'float64', copy=False + ) if batch_key is not None: - adata.var['highly_variable_nbatches'] = df['highly_variable_nbatches'].values + adata.var['highly_variable_nbatches'] = df[ + 'highly_variable_nbatches' + ].values if subset: adata._inplace_subset_var(df['highly_variable'].values) else: @@ -225,11 +235,16 @@ def _highly_variable_genes_single_batch( ) # Circumvent pandas 0.23 bug. Both sides of the assignment have dtype==float32, # but there’s still a dtype error without “.value”. - disp_std_bin[one_gene_per_bin.values] = disp_mean_bin[one_gene_per_bin.values].values + disp_std_bin[one_gene_per_bin.values] = disp_mean_bin[ + one_gene_per_bin.values + ].values disp_mean_bin[one_gene_per_bin.values] = 0 # actually do the normalization df['dispersions_norm'] = ( - df['dispersions'].values - disp_mean_bin[df['mean_bin'].values].values # use values here as index differs + df['dispersions'].values + - disp_mean_bin[ + df['mean_bin'].values + ].values # use values here as index differs ) / disp_std_bin[df['mean_bin'].values].values elif flavor == 'cell_ranger': from statsmodels import robust @@ -252,13 +267,18 @@ def _highly_variable_genes_single_batch( dispersion_norm = df['dispersions_norm'].values if n_top_genes is not None: dispersion_norm = dispersion_norm[~np.isnan(dispersion_norm)] - dispersion_norm[::-1].sort() # interestingly, np.argpartition is slightly slower + dispersion_norm[ + ::-1 + ].sort() # interestingly, np.argpartition is slightly slower if n_top_genes > adata.n_vars: logg.info('`n_top_genes` > `adata.n_var`, returning all genes.') n_top_genes = adata.n_vars disp_cut_off = dispersion_norm[n_top_genes - 1] gene_subset = np.nan_to_num(df['dispersions_norm'].values) >= disp_cut_off - logg.debug(f'the {n_top_genes} top genes correspond to a ' f'normalized dispersion cutoff of {disp_cut_off}') + logg.debug( + f'the {n_top_genes} top genes correspond to a ' + f'normalized dispersion cutoff of {disp_cut_off}' + ) else: dispersion_norm[np.isnan(dispersion_norm)] = 0 # similar to Seurat gene_subset = np.logical_and.reduce( @@ -390,7 +410,9 @@ def highly_variable_genes( This function replaces :func:`~scanpy.pp.filter_genes_dispersion`. """ - if n_top_genes is not None and not all(m is None for m in [min_disp, max_disp, min_mean, max_mean]): + if n_top_genes is not None and not all( + m is None for m in [min_disp, max_disp, min_mean, max_mean] + ): logg.info('If you pass `n_top_genes`, all cutoffs are ignored.') start = logg.info('extracting highly variable genes') @@ -476,8 +498,12 @@ def highly_variable_genes( highly_variable=np.nansum, ) ) - df.rename(columns=dict(highly_variable='highly_variable_nbatches'), inplace=True) - df['highly_variable_intersection'] = df['highly_variable_nbatches'] == len(batches) + df.rename( + columns=dict(highly_variable='highly_variable_nbatches'), inplace=True + ) + df['highly_variable_intersection'] = df['highly_variable_nbatches'] == len( + batches + ) if n_top_genes is not None: # sort genes by how often they selected as hvg within each batch and @@ -519,10 +545,16 @@ def highly_variable_genes( adata.var['highly_variable'] = df['highly_variable'].values adata.var['means'] = df['means'].values adata.var['dispersions'] = df['dispersions'].values - adata.var['dispersions_norm'] = df['dispersions_norm'].values.astype('float32', copy=False) + adata.var['dispersions_norm'] = df['dispersions_norm'].values.astype( + 'float32', copy=False + ) if batch_key is not None: - adata.var['highly_variable_nbatches'] = df['highly_variable_nbatches'].values - adata.var['highly_variable_intersection'] = df['highly_variable_intersection'].values + adata.var['highly_variable_nbatches'] = df[ + 'highly_variable_nbatches' + ].values + adata.var['highly_variable_intersection'] = df[ + 'highly_variable_intersection' + ].values if subset: adata._inplace_subset_var(df['highly_variable'].values) else: diff --git a/scanpy/preprocessing/_normalization.py b/scanpy/preprocessing/_normalization.py index 569976f361..be78bdb0c8 100644 --- a/scanpy/preprocessing/_normalization.py +++ b/scanpy/preprocessing/_normalization.py @@ -148,7 +148,9 @@ def normalize_total( if layers == 'all': layers = adata.layers.keys() elif isinstance(layers, str): - raise ValueError(f"`layers` needs to be a list of strings or 'all', not {layers!r}") + raise ValueError( + f"`layers` needs to be a list of strings or 'all', not {layers!r}" + ) view_to_actual(adata) @@ -214,7 +216,9 @@ def normalize_total( time=start, ) if key_added is not None: - logg.debug(f'and added {key_added!r}, counts per cell before normalization (adata.obs)') + logg.debug( + f'and added {key_added!r}, counts per cell before normalization (adata.obs)' + ) if copy: return adata diff --git a/scanpy/preprocessing/_pca.py b/scanpy/preprocessing/_pca.py index 12a03b60a9..fc4ec679cc 100644 --- a/scanpy/preprocessing/_pca.py +++ b/scanpy/preprocessing/_pca.py @@ -138,7 +138,9 @@ def pca( use_highly_variable = True if 'highly_variable' in adata.var.keys() else False if use_highly_variable: logg.info(' on highly variable genes') - adata_comp = adata[:, adata.var['highly_variable']] if use_highly_variable else adata + adata_comp = ( + adata[:, adata.var['highly_variable']] if use_highly_variable else adata + ) if n_comps is None: min_dim = min(adata_comp.n_vars, adata_comp.n_obs) @@ -180,7 +182,9 @@ def pca( "This may take a very large amount of memory." ) X = X.toarray() - pca_ = PCA(n_components=n_comps, svd_solver=svd_solver, random_state=random_state) + pca_ = PCA( + n_components=n_comps, svd_solver=svd_solver, random_state=random_state + ) X_pca = pca_.fit_transform(X) elif issparse(X) and zero_center: from sklearn.decomposition import PCA @@ -193,7 +197,9 @@ def pca( 'Use "arpack" (the default) or "lobpcg" instead.' ) - output = _pca_with_sparse(X, n_comps, solver=svd_solver, random_state=random_state) + output = _pca_with_sparse( + X, n_comps, solver=svd_solver, random_state=random_state + ) # this is just a wrapper for the results X_pca = output['X_pca'] pca_ = PCA(n_components=n_comps, svd_solver=svd_solver) @@ -209,7 +215,9 @@ def pca( ' the first component, e.g., might be heavily influenced by different means\n' ' the following components often resemble the exact PCA very closely' ) - pca_ = TruncatedSVD(n_components=n_comps, random_state=random_state, algorithm=svd_solver) + pca_ = TruncatedSVD( + n_components=n_comps, random_state=random_state, algorithm=svd_solver + ) X_pca = pca_.fit_transform(X) else: raise Exception("This shouldn't happen. Please open a bug report.") diff --git a/scanpy/preprocessing/_qc.py b/scanpy/preprocessing/_qc.py index f915fe738c..1f966f9d96 100644 --- a/scanpy/preprocessing/_qc.py +++ b/scanpy/preprocessing/_qc.py @@ -24,7 +24,8 @@ def _choose_mtx_rep(adata, use_raw=False, layer=None): is_layer = layer is not None if use_raw and is_layer: raise ValueError( - "Cannot use expression from both layer and raw. You provided:" f"'use_raw={use_raw}' and 'layer={layer}'" + "Cannot use expression from both layer and raw. You provided:" + f"'use_raw={use_raw}' and 'layer={layer}'" ) if is_layer: return adata.layers[layer] @@ -102,21 +103,33 @@ def describe_obs( else: obs_metrics[f"n_{var_type}_by_{expr_type}"] = np.count_nonzero(X, axis=1) if log1p: - obs_metrics[f"log1p_n_{var_type}_by_{expr_type}"] = np.log1p(obs_metrics[f"n_{var_type}_by_{expr_type}"]) + obs_metrics[f"log1p_n_{var_type}_by_{expr_type}"] = np.log1p( + obs_metrics[f"n_{var_type}_by_{expr_type}"] + ) obs_metrics[f"total_{expr_type}"] = X.sum(axis=1) if log1p: - obs_metrics[f"log1p_total_{expr_type}"] = np.log1p(obs_metrics[f"total_{expr_type}"]) + obs_metrics[f"log1p_total_{expr_type}"] = np.log1p( + obs_metrics[f"total_{expr_type}"] + ) if percent_top: percent_top = sorted(percent_top) proportions = top_segment_proportions(X, percent_top) for i, n in enumerate(percent_top): - obs_metrics[f"pct_{expr_type}_in_top_{n}_{var_type}"] = proportions[:, i] * 100 + obs_metrics[f"pct_{expr_type}_in_top_{n}_{var_type}"] = ( + proportions[:, i] * 100 + ) for qc_var in qc_vars: - obs_metrics[f"total_{expr_type}_{qc_var}"] = X[:, adata.var[qc_var].values].sum(axis=1) + obs_metrics[f"total_{expr_type}_{qc_var}"] = X[:, adata.var[qc_var].values].sum( + axis=1 + ) if log1p: - obs_metrics[f"log1p_total_{expr_type}_{qc_var}"] = np.log1p(obs_metrics[f"total_{expr_type}_{qc_var}"]) + obs_metrics[f"log1p_total_{expr_type}_{qc_var}"] = np.log1p( + obs_metrics[f"total_{expr_type}_{qc_var}"] + ) obs_metrics[f"pct_{expr_type}_{qc_var}"] = ( - obs_metrics[f"total_{expr_type}_{qc_var}"] / obs_metrics[f"total_{expr_type}"] * 100 + obs_metrics[f"total_{expr_type}_{qc_var}"] + / obs_metrics[f"total_{expr_type}"] + * 100 ) if inplace: adata.obs[obs_metrics.columns] = obs_metrics @@ -180,11 +193,17 @@ def describe_var( var_metrics["n_cells_by_{expr_type}"] = np.count_nonzero(X, axis=0) var_metrics["mean_{expr_type}"] = X.mean(axis=0) if log1p: - var_metrics["log1p_mean_{expr_type}"] = np.log1p(var_metrics["mean_{expr_type}"]) - var_metrics["pct_dropout_by_{expr_type}"] = (1 - var_metrics["n_cells_by_{expr_type}"] / X.shape[0]) * 100 + var_metrics["log1p_mean_{expr_type}"] = np.log1p( + var_metrics["mean_{expr_type}"] + ) + var_metrics["pct_dropout_by_{expr_type}"] = ( + 1 - var_metrics["n_cells_by_{expr_type}"] / X.shape[0] + ) * 100 var_metrics["total_{expr_type}"] = np.ravel(X.sum(axis=0)) if log1p: - var_metrics["log1p_total_{expr_type}"] = np.log1p(var_metrics["total_{expr_type}"]) + var_metrics["log1p_total_{expr_type}"] = np.log1p( + var_metrics["total_{expr_type}"] + ) # Relabel new_colnames = [] for col in var_metrics.columns: @@ -358,7 +377,9 @@ def top_proportions_sparse_csr(data, indptr, n): return values -def top_segment_proportions(mtx: Union[np.array, spmatrix], ns: Collection[int]) -> np.ndarray: +def top_segment_proportions( + mtx: Union[np.array, spmatrix], ns: Collection[int] +) -> np.ndarray: """ Calculates total percentage of counts in top ns genes. @@ -382,11 +403,15 @@ def top_segment_proportions(mtx: Union[np.array, spmatrix], ns: Collection[int]) return top_segment_proportions_dense(mtx, ns) -def top_segment_proportions_dense(mtx: Union[np.array, spmatrix], ns: Collection[int]) -> np.ndarray: +def top_segment_proportions_dense( + mtx: Union[np.array, spmatrix], ns: Collection[int] +) -> np.ndarray: # Currently ns is considered to be 1 indexed ns = np.sort(ns) sums = mtx.sum(axis=1) - partitioned = np.apply_along_axis(np.partition, 1, mtx, mtx.shape[1] - ns)[:, ::-1][:, : ns[-1]] + partitioned = np.apply_along_axis(np.partition, 1, mtx, mtx.shape[1] - ns)[:, ::-1][ + :, : ns[-1] + ] values = np.zeros((mtx.shape[0], len(ns))) acc = np.zeros(mtx.shape[0]) prev = 0 diff --git a/scanpy/preprocessing/_recipes.py b/scanpy/preprocessing/_recipes.py index 8d6f43b364..d211bcc20a 100644 --- a/scanpy/preprocessing/_recipes.py +++ b/scanpy/preprocessing/_recipes.py @@ -47,7 +47,9 @@ def recipe_weinreb17( adata = adata.copy() if log: pp.log1p(adata) - adata.X = normalize_per_cell_weinreb16_deprecated(adata.X, max_fraction=0.05, mult_with_mean=True) + adata.X = normalize_per_cell_weinreb16_deprecated( + adata.X, max_fraction=0.05, mult_with_mean=True + ) gene_subset = filter_genes_cv_deprecated(adata.X, mean_threshold, cv_threshold) adata._inplace_subset_var(gene_subset) # this modifies the object itself X_pca = pp.pca( @@ -61,7 +63,9 @@ def recipe_weinreb17( return adata if copy else None -def recipe_seurat(adata: AnnData, log: bool = True, plot: bool = False, copy: bool = False) -> Optional[AnnData]: +def recipe_seurat( + adata: AnnData, log: bool = True, plot: bool = False, copy: bool = False +) -> Optional[AnnData]: """\ Normalization and filtering as of Seurat [Satija15]_. @@ -75,7 +79,9 @@ def recipe_seurat(adata: AnnData, log: bool = True, plot: bool = False, copy: bo pp.filter_cells(adata, min_genes=200) pp.filter_genes(adata, min_cells=3) normalize_total(adata, target_sum=1e4) - filter_result = filter_genes_dispersion(adata.X, min_mean=0.0125, max_mean=3, min_disp=0.5, log=not log) + filter_result = filter_genes_dispersion( + adata.X, min_mean=0.0125, max_mean=3, min_disp=0.5, log=not log + ) if plot: from ..plotting import ( _preprocessing as ppp, @@ -146,7 +152,9 @@ def recipe_zheng17( pp.filter_genes(adata, min_counts=1) # normalize with total UMI count per cell normalize_total(adata, key_added='n_counts_all') - filter_result = filter_genes_dispersion(adata.X, flavor='cell_ranger', n_top_genes=n_top_genes, log=False) + filter_result = filter_genes_dispersion( + adata.X, flavor='cell_ranger', n_top_genes=n_top_genes, log=False + ) if plot: # should not import at the top of the file from ..plotting import _preprocessing as ppp diff --git a/scanpy/preprocessing/_simple.py b/scanpy/preprocessing/_simple.py index 195c2b110c..344ea2022c 100644 --- a/scanpy/preprocessing/_simple.py +++ b/scanpy/preprocessing/_simple.py @@ -119,7 +119,9 @@ def filter_cells( """ if copy: logg.warning('`copy` is deprecated, use `inplace` instead.') - n_given_options = sum(option is not None for option in [min_genes, min_counts, max_genes, max_counts]) + n_given_options = sum( + option is not None for option in [min_genes, min_counts, max_genes, max_counts] + ) if n_given_options != 1: raise ValueError( 'Only provide one of the optional parameters `min_counts`, ' @@ -141,7 +143,9 @@ def filter_cells( X = data # proceed with processing the data matrix min_number = min_counts if min_genes is None else min_genes max_number = max_counts if max_genes is None else max_genes - number_per_cell = np.sum(X if min_genes is None and max_genes is None else X > 0, axis=1) + number_per_cell = np.sum( + X if min_genes is None and max_genes is None else X > 0, axis=1 + ) if issparse(X): number_per_cell = number_per_cell.A1 if min_number is not None: @@ -154,10 +158,18 @@ def filter_cells( msg = f'filtered out {s} cells that have ' if min_genes is not None or min_counts is not None: msg += 'less than ' - msg += f'{min_genes} genes expressed' if min_counts is None else f'{min_counts} counts' + msg += ( + f'{min_genes} genes expressed' + if min_counts is None + else f'{min_counts} counts' + ) if max_genes is not None or max_counts is not None: msg += 'more than ' - msg += f'{max_genes} genes expressed' if max_counts is None else f'{max_counts} counts' + msg += ( + f'{max_genes} genes expressed' + if max_counts is None + else f'{max_counts} counts' + ) logg.info(msg) return cell_subset, number_per_cell @@ -211,7 +223,9 @@ def filter_genes( """ if copy: logg.warning('`copy` is deprecated, use `inplace` instead.') - n_given_options = sum(option is not None for option in [min_cells, min_counts, max_cells, max_counts]) + n_given_options = sum( + option is not None for option in [min_cells, min_counts, max_cells, max_counts] + ) if n_given_options != 1: raise ValueError( 'Only provide one of the optional parameters `min_counts`, ' @@ -241,7 +255,9 @@ def filter_genes( X = data # proceed with processing the data matrix min_number = min_counts if min_cells is None else min_cells max_number = max_counts if max_cells is None else max_cells - number_per_gene = np.sum(X if min_cells is None and max_cells is None else X > 0, axis=0) + number_per_gene = np.sum( + X if min_cells is None and max_cells is None else X > 0, axis=0 + ) if issparse(X): number_per_gene = number_per_gene.A1 if min_number is not None: @@ -254,10 +270,14 @@ def filter_genes( msg = f'filtered out {s} genes that are detected ' if min_cells is not None or min_counts is not None: msg += 'in less than ' - msg += f'{min_cells} cells' if min_counts is None else f'{min_counts} counts' + msg += ( + f'{min_cells} cells' if min_counts is None else f'{min_counts} counts' + ) if max_cells is not None or max_counts is not None: msg += 'in more than ' - msg += f'{max_cells} cells' if max_counts is None else f'{max_counts} counts' + msg += ( + f'{max_cells} cells' if max_counts is None else f'{max_counts} counts' + ) logg.info(msg) return gene_subset, number_per_gene @@ -303,13 +323,17 @@ def log1p( ------- Returns or updates `data`, depending on `copy`. """ - _check_array_function_arguments(chunked=chunked, chunk_size=chunk_size, layer=layer, obsm=obsm) + _check_array_function_arguments( + chunked=chunked, chunk_size=chunk_size, layer=layer, obsm=obsm + ) return log1p_array(X, copy=copy, base=base) @log1p.register(spmatrix) def log1p_sparse(X, *, base: Optional[Number] = None, copy: bool = False): - X = check_array(X, accept_sparse=("csr", "csc"), dtype=(np.float64, np.float32), copy=copy) + X = check_array( + X, accept_sparse=("csr", "csc"), dtype=(np.float64, np.float32), copy=copy + ) X.data = log1p(X.data, copy=False, base=base) return X @@ -323,7 +347,9 @@ def log1p_array(X, *, base: Optional[Number] = None, copy: bool = False): X = X.astype(np.floating) else: X = X.copy() - elif not (np.issubdtype(X.dtype, np.floating) or np.issubdtype(X.dtype, np.complex)): + elif not ( + np.issubdtype(X.dtype, np.floating) or np.issubdtype(X.dtype, np.complex) + ): X = X.astype(np.floating) np.log1p(X, out=X) if base is not None: @@ -350,7 +376,9 @@ def log1p_anndata( if chunked: if (layer is not None) or (obsm is not None): - raise NotImplementedError("Currently cannot perform chunked operations on arrays not stored in X.") + raise NotImplementedError( + "Currently cannot perform chunked operations on arrays not stored in X." + ) for chunk, start, end in adata.chunked_X(chunk_size): adata.X[start:end] = log1p(chunk, base=base, copy=False) else: @@ -492,7 +520,9 @@ def normalize_per_cell( start = logg.info('normalizing by total count per cell') adata = data.copy() if copy else data if counts_per_cell is None: - cell_subset, counts_per_cell = materialize_as_ndarray(filter_cells(adata.X, min_counts=min_counts)) + cell_subset, counts_per_cell = materialize_as_ndarray( + filter_cells(adata.X, min_counts=min_counts) + ) adata.obs[key_n_counts] = counts_per_cell adata._inplace_subset_obs(cell_subset) counts_per_cell = counts_per_cell[cell_subset] @@ -668,7 +698,9 @@ def _regress_out_chunk(data): else: regres = regressors try: - result = sm.GLM(data_chunk[:, col_index], regres, family=sm.families.Gaussian()).fit() + result = sm.GLM( + data_chunk[:, col_index], regres, family=sm.families.Gaussian() + ).fit() new_column = result.resid_response except PerfectSeparationError: # this emulates R's behavior logg.warning('Encountered PerfectSeparationError, setting to 0 as in R.') @@ -720,7 +752,9 @@ def scale( annotated with `'mean'` and `'std'` in `adata.var`. """ _check_array_function_arguments(layer=layer, obsm=obsm) - return scale_array(data, zero_center=zero_center, max_value=max_value, copy=copy) # noqa: F821 + return scale_array( + data, zero_center=zero_center, max_value=max_value, copy=copy # noqa: F821 + ) @scale.register(np.ndarray) @@ -740,7 +774,10 @@ def scale_array( ) if np.issubdtype(X.dtype, np.integer): - logg.info('... as scaling leads to float results, integer ' 'input is cast to float, returning copy.') + logg.info( + '... as scaling leads to float results, integer ' + 'input is cast to float, returning copy.' + ) X = X.astype(float) mean, var = _get_mean_var(X) @@ -777,7 +814,10 @@ def scale_sparse( ): # need to add the following here to make inplace logic work if zero_center: - logg.info("... as `zero_center=True`, sparse input is " "densified and may lead to large memory consumption") + logg.info( + "... as `zero_center=True`, sparse input is " + "densified and may lead to large memory consumption" + ) X = X.toarray() copy = False # Since the data has been copied return scale_array( @@ -911,7 +951,9 @@ def downsample_counts( total_counts_call = total_counts is not None counts_per_cell_call = counts_per_cell is not None if total_counts_call is counts_per_cell_call: - raise ValueError("Must specify exactly one of `total_counts` or `counts_per_cell`.") + raise ValueError( + "Must specify exactly one of `total_counts` or `counts_per_cell`." + ) if copy: adata = adata.copy() if total_counts_call: diff --git a/scanpy/preprocessing/_utils.py b/scanpy/preprocessing/_utils.py index 303a4d58d2..45ec781661 100644 --- a/scanpy/preprocessing/_utils.py +++ b/scanpy/preprocessing/_utils.py @@ -35,7 +35,9 @@ def sparse_mean_variance_axis(mtx: sparse.spmatrix, axis: int): else: raise ValueError("This function only works on sparse csr and csc matrices") if axis == ax_minor: - return sparse_mean_var_major_axis(mtx.data, mtx.indices, mtx.indptr, *shape, np.float64) + return sparse_mean_var_major_axis( + mtx.data, mtx.indices, mtx.indptr, *shape, np.float64 + ) else: return sparse_mean_var_minor_axis(mtx.data, mtx.indices, *shape, np.float64) diff --git a/scanpy/queries/_queries.py b/scanpy/queries/_queries.py index c206a5262e..7dff67565f 100644 --- a/scanpy/queries/_queries.py +++ b/scanpy/queries/_queries.py @@ -60,9 +60,13 @@ def simple_query( try: from pybiomart import Server except ImportError: - raise ImportError("This method requires the `pybiomart` module to be installed.") + raise ImportError( + "This method requires the `pybiomart` module to be installed." + ) server = Server(host, use_cache=use_cache) - dataset = server.marts["ENSEMBL_MART_ENSEMBL"].datasets["{}_gene_ensembl".format(org)] + dataset = server.marts["ENSEMBL_MART_ENSEMBL"].datasets[ + "{}_gene_ensembl".format(org) + ] res = dataset.query(attributes=attrs, filters=filters, use_attr_names=True) return res @@ -260,13 +264,16 @@ def enrich( try: from gprofiler import GProfiler except ImportError: - raise ImportError("This method requires the `gprofiler-official` module to be installed.") + raise ImportError( + "This method requires the `gprofiler-official` module to be installed." + ) gprofiler = GProfiler(user_agent="scanpy", return_dataframe=True) gprofiler_kwargs = dict(gprofiler_kwargs) for k in ["organism"]: if gprofiler_kwargs.get(k) is not None: raise ValueError( - f"Argument `{k}` should be passed directly through `enrich`, " "not through `gprofiler_kwargs`" + f"Argument `{k}` should be passed directly through `enrich`, " + "not through `gprofiler_kwargs`" ) return gprofiler.profile(container, organism=org, **gprofiler_kwargs) diff --git a/scanpy/readwrite.py b/scanpy/readwrite.py index c31f8f042f..87d2055fc4 100644 --- a/scanpy/readwrite.py +++ b/scanpy/readwrite.py @@ -214,7 +214,8 @@ def _read_legacy_10x_h5(filename, *, genome=None, start=None): genome = children[0] elif genome not in children: raise ValueError( - f"Could not find genome '{genome}' in '{filename}'. " f'Available genomes are: {children}' + f"Could not find genome '{genome}' in '{filename}'. " + f'Available genomes are: {children}' ) dsets = {} for node in f.walk_nodes('/' + genome, 'Array'): @@ -372,19 +373,26 @@ def read_visium( for f in files.values(): if not f.exists(): if any(x in str(f) for x in ["hires_image", "lowres_image"]): - logg.warning(f"You seem to be missing an image file.\n" f"Could not find '{f}'.") + logg.warning( + f"You seem to be missing an image file.\n" + f"Could not find '{f}'." + ) else: raise OSError(f"Could not find '{f}'") adata.uns["spatial"][library_id]['images'] = dict() for res in ['hires', 'lowres']: try: - adata.uns["spatial"][library_id]['images'][res] = imread(str(files[f'{res}_image'])) + adata.uns["spatial"][library_id]['images'][res] = imread( + str(files[f'{res}_image']) + ) except Exception: raise OSError(f"Could not find '{res}_image'") # read json scalefactors - adata.uns["spatial"][library_id]['scalefactors'] = json.loads(files['scalefactors_json_file'].read_bytes()) + adata.uns["spatial"][library_id]['scalefactors'] = json.loads( + files['scalefactors_json_file'].read_bytes() + ) adata.uns["spatial"][library_id]["metadata"] = { k: (str(attrs[k], "utf-8") if isinstance(attrs[k], bytes) else attrs[k]) @@ -406,7 +414,9 @@ def read_visium( adata.obs = adata.obs.join(positions, how="left") - adata.obsm['spatial'] = adata.obs[['pxl_row_in_fullres', 'pxl_col_in_fullres']].to_numpy() + adata.obsm['spatial'] = adata.obs[ + ['pxl_row_in_fullres', 'pxl_col_in_fullres'] + ].to_numpy() adata.obs.drop( columns=['barcode', 'pxl_row_in_fullres', 'pxl_col_in_fullres'], inplace=True, @@ -416,7 +426,9 @@ def read_visium( if source_image_path is not None: # get an absolute path source_image_path = str(Path(source_image_path).resolve()) - adata.uns["spatial"][library_id]["metadata"]["source_image_path"] = str(source_image_path) + adata.uns["spatial"][library_id]["metadata"]["source_image_path"] = str( + source_image_path + ) return adata @@ -477,7 +489,9 @@ def read_10x_mtx( if genefile_exists or not gex_only: return adata else: - gex_rows = list(map(lambda x: x == 'Gene Expression', adata.var['feature_types'])) + gex_rows = list( + map(lambda x: x == 'Gene Expression', adata.var['feature_types']) + ) return adata[:, gex_rows].copy() @@ -546,7 +560,9 @@ def _read_v3_10x_mtx( else: raise ValueError("`var_names` needs to be 'gene_symbols' or 'gene_ids'") adata.var['feature_types'] = genes[2].values - adata.obs_names = pd.read_csv(path / f'{prefix}barcodes.tsv.gz', header=None)[0].values + adata.obs_names = pd.read_csv(path / f'{prefix}barcodes.tsv.gz', header=None)[ + 0 + ].values return adata @@ -596,7 +612,9 @@ def write( if ext == 'csv': adata.write_csvs(filename) else: - adata.write(filename, compression=compression, compression_opts=compression_opts) + adata.write( + filename, compression=compression, compression_opts=compression_opts + ) # ------------------------------------------------------------------------------- @@ -604,7 +622,9 @@ def write( # ------------------------------------------------------------------------------- -def read_params(filename: Union[Path, str], asheader: bool = False) -> Dict[str, Union[int, float, bool, str, None]]: +def read_params( + filename: Union[Path, str], asheader: bool = False +) -> Dict[str, Union[int, float, bool, str, None]]: """\ Read parameter dictionary from text file. @@ -679,7 +699,9 @@ def _read( **kwargs, ): if ext is not None and ext not in avail_exts: - raise ValueError('Please provide one of the available extensions.\n' f'{avail_exts}') + raise ValueError( + 'Please provide one of the available extensions.\n' f'{avail_exts}' + ) else: ext = is_valid_filename(filename, return_ext=True) is_present = _check_datafile_present_and_download(filename, backup_url=backup_url) @@ -693,7 +715,9 @@ def _read( logg.debug(f'reading sheet {sheet} from file {filename}') return read_hdf(filename, sheet) # read other file types - path_cache = settings.cachedir / _slugify(filename).replace('.' + ext, '.h5ad') # type: Path + path_cache = settings.cachedir / _slugify(filename).replace( + '.' + ext, '.h5ad' + ) # type: Path if path_cache.suffix in {'.gz', '.bz2'}: path_cache = path_cache.with_suffix('') if cache and path_cache.is_file(): @@ -732,7 +756,10 @@ def _read( else: raise ValueError(f'Unknown extension {ext}.') if cache: - logg.info(f'... writing an {settings.file_format_data} ' 'cache file to speedup reading next time') + logg.info( + f'... writing an {settings.file_format_data} ' + 'cache file to speedup reading next time' + ) if cache_compression is _empty: cache_compression = settings.cache_compression if not path_cache.parent.is_dir(): @@ -876,7 +903,9 @@ def get_used_files(): """Get files used by processes with name scanpy.""" import psutil - loop_over_scanpy_processes = (proc for proc in psutil.process_iter() if proc.name() == 'scanpy') + loop_over_scanpy_processes = ( + proc for proc in psutil.process_iter() if proc.name() == 'scanpy' + ) filenames = [] for proc in loop_over_scanpy_processes: try: @@ -938,7 +967,10 @@ def _check_datafile_present_and_download(path, backup_url=None): return True if backup_url is None: return False - logg.info(f'try downloading from url\n{backup_url}\n' '... this may take a while but only happens once') + logg.info( + f'try downloading from url\n{backup_url}\n' + '... this may take a while but only happens once' + ) if not path.parent.is_dir(): logg.info(f'creating directory {path.parent}/ for saving data') path.parent.mkdir(parents=True) @@ -953,7 +985,8 @@ def is_valid_filename(filename: Path, return_ext=False): if len(ext) > 2: logg.warning( - f'Your filename has more than two extensions: {ext}.\n' f'Only considering the two last: {ext[-2:]}.' + f'Your filename has more than two extensions: {ext}.\n' + f'Only considering the two last: {ext[-2:]}.' ) ext = ext[-2:] diff --git a/scanpy/tests/external/test_hashsolo.py b/scanpy/tests/external/test_hashsolo.py index 4d3a223f28..8ab8df0e61 100644 --- a/scanpy/tests/external/test_hashsolo.py +++ b/scanpy/tests/external/test_hashsolo.py @@ -23,7 +23,9 @@ def test_cell_demultiplexing(): sce.pp.hashsolo(test_data, test_data.obs.columns) doublets = ["Doublet"] * 10 - classes = list(np.repeat(np.arange(10), 98).reshape(98, 10, order="F").ravel().astype(str)) + classes = list( + np.repeat(np.arange(10), 98).reshape(98, 10, order="F").ravel().astype(str) + ) negatives = ["Negative"] * 10 classification = doublets + classes + negatives assert all(test_data.obs["Classification"].astype(str) == classification) diff --git a/scanpy/tests/external/test_wishbone.py b/scanpy/tests/external/test_wishbone.py index 9baca5f877..fc3cf71901 100644 --- a/scanpy/tests/external/test_wishbone.py +++ b/scanpy/tests/external/test_wishbone.py @@ -20,4 +20,6 @@ def test_run_wishbone(): components=[2, 3], num_waypoints=150, ) - assert all([k in adata.obs for k in ['trajectory_wishbone', 'branch_wishbone']]), "Run Wishbone Error!" + assert all( + [k in adata.obs for k in ['trajectory_wishbone', 'branch_wishbone']] + ), "Run Wishbone Error!" diff --git a/scanpy/tests/helpers.py b/scanpy/tests/helpers.py index c2d4219fdd..f6bb214550 100644 --- a/scanpy/tests/helpers.py +++ b/scanpy/tests/helpers.py @@ -37,7 +37,7 @@ def check_rep_mutation(func, X, *, fields=["layer", "obsm"], **kwargs): ) np.testing.assert_array_equal(asarray(adata_X.X), result_array) -<<<<<<< HEAD + # Unmodified fields for field in fields: np.testing.assert_array_equal(X_array, asarray(adatas_proc[field].X)) @@ -49,11 +49,6 @@ def check_rep_mutation(func, X, *, fields=["layer", "obsm"], **kwargs): sc.get._get_obs_rep(adatas_proc[field_a], **{field_b: field_b}) ) np.testing.assert_array_equal(X_array, result_array) -======= - assert np.array_equal(asarray(adata_layer.X), asarray(adata_layer.obsm["obsm"])) - assert np.array_equal(asarray(adata_obsm.X), asarray(adata_obsm.layers["layer"])) - assert np.array_equal(asarray(adata_X.layers["layer"]), asarray(adata_X.obsm["obsm"])) ->>>>>>> 40dc2c3b (add flake8 pre-commit) def check_rep_results(func, X, *, fields=["layer", "obsm"], **kwargs): diff --git a/scanpy/tests/notebooks/test_paga_paul15_subsampled.py b/scanpy/tests/notebooks/test_paga_paul15_subsampled.py index cbf7c0d252..839d93f40a 100644 --- a/scanpy/tests/notebooks/test_paga_paul15_subsampled.py +++ b/scanpy/tests/notebooks/test_paga_paul15_subsampled.py @@ -115,7 +115,9 @@ def test_paga_paul15_subsampled(image_comparer, plt): adata.obs['distance'] = adata.obs['dpt_pseudotime'] - _, axs = plt.subplots(ncols=3, figsize=(6, 2.5), gridspec_kw={'wspace': 0.05, 'left': 0.12}) + _, axs = plt.subplots( + ncols=3, figsize=(6, 2.5), gridspec_kw={'wspace': 0.05, 'left': 0.12} + ) plt.subplots_adjust(left=0.05, right=0.98, top=0.82, bottom=0.2) for ipath, (descr, path) in enumerate(paths): _, data = sc.pl.paga_path( diff --git a/scanpy/tests/notebooks/test_pbmc3k.py b/scanpy/tests/notebooks/test_pbmc3k.py index b7198d972b..0146c4b4de 100644 --- a/scanpy/tests/notebooks/test_pbmc3k.py +++ b/scanpy/tests/notebooks/test_pbmc3k.py @@ -30,7 +30,9 @@ def test_pbmc3k(image_comparer): save_and_compare_images = image_comparer(ROOT, FIGS, tol=20) - adata = sc.read('./data/pbmc3k_raw.h5ad', backup_url='http://falexwolf.de/data/pbmc3k_raw.h5ad') + adata = sc.read( + './data/pbmc3k_raw.h5ad', backup_url='http://falexwolf.de/data/pbmc3k_raw.h5ad' + ) # Preprocessing @@ -43,7 +45,9 @@ def test_pbmc3k(image_comparer): mito_genes = [name for name in adata.var_names if name.startswith('MT-')] # for each cell compute fraction of counts in mito genes vs. all genes # the `.A1` is only necessary as X is sparse to transform to a dense array after summing - adata.obs['percent_mito'] = np.sum(adata[:, mito_genes].X, axis=1).A1 / np.sum(adata.X, axis=1).A1 + adata.obs['percent_mito'] = ( + np.sum(adata[:, mito_genes].X, axis=1).A1 / np.sum(adata.X, axis=1).A1 + ) # add the total counts per cell as observations-annotation to adata adata.obs['n_counts'] = adata.X.sum(axis=1).A1 @@ -140,5 +144,7 @@ def test_pbmc3k(image_comparer): # sc.pl.umap(adata, color='louvain', legend_loc='on data', title='', frameon=False, show=False) # save_and_compare_images('umap_3') - sc.pl.violin(adata, ['CST3', 'NKG7', 'PPBP'], groupby='louvain', rotation=90, show=False) + sc.pl.violin( + adata, ['CST3', 'NKG7', 'PPBP'], groupby='louvain', rotation=90, show=False + ) save_and_compare_images('violin_2') diff --git a/scanpy/tests/test_combat.py b/scanpy/tests/test_combat.py index 79be9a10ea..295667b909 100644 --- a/scanpy/tests/test_combat.py +++ b/scanpy/tests/test_combat.py @@ -37,7 +37,9 @@ def test_covariates(): adata.obs['cat2'] = np.random.binomial(2, 0.1, size=(adata.n_obs)) adata.obs['num1'] = np.random.normal(size=(adata.n_obs)) - X2 = sc.pp.combat(adata, key=key, covariates=['cat1', 'cat2', 'num1'], inplace=False) + X2 = sc.pp.combat( + adata, key=key, covariates=['cat1', 'cat2', 'num1'], inplace=False + ) sc.pp.combat(adata, key=key, covariates=['cat1', 'cat2', 'num1'], inplace=True) assert X1.shape == X2.shape diff --git a/scanpy/tests/test_datasets.py b/scanpy/tests/test_datasets.py index 30e3d497a8..50332de6f9 100644 --- a/scanpy/tests/test_datasets.py +++ b/scanpy/tests/test_datasets.py @@ -63,7 +63,10 @@ def test_ebi_expression_atlas(tmp_dataset_dir): def test_krumsiek11(tmp_dataset_dir): adata = sc.datasets.krumsiek11() assert adata.shape == (640, 11) - assert all(np.unique(adata.obs["cell_type"]) == np.array(["Ery", "Mk", "Mo", "Neu", "progenitor"])) + assert all( + np.unique(adata.obs["cell_type"]) + == np.array(["Ery", "Mk", "Mo", "Neu", "progenitor"]) + ) def test_blobs(): @@ -99,14 +102,18 @@ def test_visium_datasets(tmp_dataset_dir, tmpdir): # Test that downloading tissue image works mbrain = sc.datasets.visium_sge("V1_Adult_Mouse_Brain", include_hires_tiff=True) expected_image_path = sc.settings.datasetdir / "V1_Adult_Mouse_Brain" / "image.tif" - image_path = Path(mbrain.uns["spatial"]["V1_Adult_Mouse_Brain"]["metadata"]["source_image_path"]) + image_path = Path( + mbrain.uns["spatial"]["V1_Adult_Mouse_Brain"]["metadata"]["source_image_path"] + ) assert image_path == expected_image_path # Test that tissue image exists and is a valid image file assert image_path.exists() # Test that tissue image is a tif image file (using `file`) - process = subprocess.run(['file', '--mime-type', image_path], stdout=subprocess.PIPE) + process = subprocess.run( + ['file', '--mime-type', image_path], stdout=subprocess.PIPE + ) output = process.stdout.strip().decode() # make process output string assert output == str(image_path) + ': image/tiff' diff --git a/scanpy/tests/test_embedding_plots.py b/scanpy/tests/test_embedding_plots.py index e21a9caf82..58db03d965 100644 --- a/scanpy/tests/test_embedding_plots.py +++ b/scanpy/tests/test_embedding_plots.py @@ -30,7 +30,9 @@ def adata(): from sklearn.cluster import DBSCAN empty_pixel = np.array([1.0, 1.0, 1.0, 0]).reshape(1, 1, -1) - image = imread(Path(sc.__file__).parent.parent / "docs/_static/img/Scanpy_Logo_RGB.png") + image = imread( + Path(sc.__file__).parent.parent / "docs/_static/img/Scanpy_Logo_RGB.png" + ) x, y = np.where(np.logical_and.reduce(~np.equal(image, empty_pixel), axis=2)) # Just using to calculate the hex coords @@ -69,7 +71,9 @@ def adata(): adata.obs["label_missing"][::2] = np.nan adata.obs["1_missing"] = adata.obs_vector("1") - adata.obs.loc[adata.obsm["spatial"][:, 0] < adata.obsm["spatial"][:, 0].mean(), "1_missing"] = np.nan + adata.obs.loc[ + adata.obsm["spatial"][:, 0] < adata.obsm["spatial"][:, 0].mean(), "1_missing" + ] = np.nan return adata @@ -157,7 +161,9 @@ def test_missing_values_categorical( legend_loc, groupsfunc, ): - save_and_compare_images = image_comparer(MISSING_VALUES_ROOT, MISSING_VALUES_FIGS, tol=15) + save_and_compare_images = image_comparer( + MISSING_VALUES_ROOT, MISSING_VALUES_FIGS, tol=15 + ) base_name = fixture_request.node.name # Passing through a dict so it's easier to use default values @@ -173,8 +179,12 @@ def test_missing_values_categorical( save_and_compare_images(base_name) -def test_missing_values_continuous(fixture_request, image_comparer, adata, plotfunc, na_color, legend_loc, vbounds): - save_and_compare_images = image_comparer(MISSING_VALUES_ROOT, MISSING_VALUES_FIGS, tol=15) +def test_missing_values_continuous( + fixture_request, image_comparer, adata, plotfunc, na_color, legend_loc, vbounds +): + save_and_compare_images = image_comparer( + MISSING_VALUES_ROOT, MISSING_VALUES_FIGS, tol=15 + ) base_name = fixture_request.node.name # Passing through a dict so it's easier to use default values @@ -258,9 +268,13 @@ def test_spatial_general(image_comparer): # general coordinates save_and_compare_images = image_comparer(ROOT, FIGS, tol=15) adata = sc.read_visium(HERE / '_data' / 'visium_data' / '1.0.0') adata.obs = adata.obs.astype({'array_row': 'str'}) - spatial_metadata = adata.uns.pop("spatial") # spatial data don't have imgs, so remove entry from uns + spatial_metadata = adata.uns.pop( + "spatial" + ) # spatial data don't have imgs, so remove entry from uns # Required argument for now - spot_size = list(spatial_metadata.values())[0]["scalefactors"]["spot_diameter_fullres"] + spot_size = list(spatial_metadata.values())[0]["scalefactors"][ + "spot_diameter_fullres" + ] sc.pl.spatial(adata, show=False, spot_size=spot_size) save_and_compare_images('master_spatial_general_nocol') @@ -333,8 +347,12 @@ def equivalent_spatial_plotters_no_img(equivalent_spatial_plotters): pytest.param({"bw": True}, id="bw"), # Shape of the image for particular fixture, should not be hardcoded like this pytest.param({"img": np.ones((774, 1755, 4)), "scale_factor": 1.0}, id="img"), - pytest.param({"na_color": (0, 0, 0, 0), "color": "1_missing"}, id="na_color.transparent"), - pytest.param({"na_color": "lightgray", "color": "1_missing"}, id="na_color.lightgray"), + pytest.param( + {"na_color": (0, 0, 0, 0), "color": "1_missing"}, id="na_color.transparent" + ), + pytest.param( + {"na_color": "lightgray", "color": "1_missing"}, id="na_color.lightgray" + ), ] ) def spatial_kwargs(request): @@ -361,7 +379,9 @@ def test_manual_equivalency(equivalent_spatial_plotters, tmpdir, spatial_kwargs) check_images(orig_pth, removed_pth, tol=1) -def test_manual_equivalency_no_img(equivalent_spatial_plotters_no_img, tmpdir, spatial_kwargs): +def test_manual_equivalency_no_img( + equivalent_spatial_plotters_no_img, tmpdir, spatial_kwargs +): if "bw" in spatial_kwargs: # Has no meaning when there is no image pytest.skip() @@ -386,7 +406,9 @@ def test_white_background_vs_no_img(adata, tmpdir, spatial_kwargs): # These arguments don't make sense for this check pytest.skip() - white_background = np.ones_like(adata.uns["spatial"]["scanpy_img"]["images"]["hires"]) + white_background = np.ones_like( + adata.uns["spatial"]["scanpy_img"]["images"]["hires"] + ) TESTDIR = Path(tmpdir) white_pth = TESTDIR / "white_background.png" noimg_pth = TESTDIR / "no_img.png" @@ -410,7 +432,9 @@ def test_spatial_na_color(adata, tmpdir): """ Check that na_color defaults to transparent when an image is present, light gray when not. """ - white_background = np.ones_like(adata.uns["spatial"]["scanpy_img"]["images"]["hires"]) + white_background = np.ones_like( + adata.uns["spatial"]["scanpy_img"]["images"]["hires"] + ) TESTDIR = Path(tmpdir) lightgray_pth = TESTDIR / "lightgray.png" transparent_pth = TESTDIR / "transparent.png" diff --git a/scanpy/tests/test_filter_rank_genes_groups.py b/scanpy/tests/test_filter_rank_genes_groups.py index 9989a81989..91aff2d27c 100644 --- a/scanpy/tests/test_filter_rank_genes_groups.py +++ b/scanpy/tests/test_filter_rank_genes_groups.py @@ -47,7 +47,9 @@ def test_filter_rank_genes_groups(): 'max_out_group_fraction': 0.5, } - rank_genes_groups(adata, 'bulk_labels', reference='Dendritic', method='wilcoxon', n_genes=5) + rank_genes_groups( + adata, 'bulk_labels', reference='Dendritic', method='wilcoxon', n_genes=5 + ) filter_rank_genes_groups(**args) assert np.array_equal( diff --git a/scanpy/tests/test_get.py b/scanpy/tests/test_get.py index 1050e4ad55..7177fee921 100644 --- a/scanpy/tests/test_get.py +++ b/scanpy/tests/test_get.py @@ -39,8 +39,12 @@ def adata(): """ return AnnData( X=np.ones((2, 2)), - obs=pd.DataFrame({"obs1": [0, 1], "obs2": ["a", "b"]}, index=["cell1", "cell2"]), - var=pd.DataFrame({"gene_symbols": ["genesymbol1", "genesymbol2"]}, index=["gene1", "gene2"]), + obs=pd.DataFrame( + {"obs1": [0, 1], "obs2": ["a", "b"]}, index=["cell1", "cell2"] + ), + var=pd.DataFrame( + {"gene_symbols": ["genesymbol1", "genesymbol2"]}, index=["gene1", "gene2"] + ), layers={"double": sparse.csr_matrix(np.ones((2, 2)), dtype=int) * 2}, dtype=int, ) @@ -65,7 +69,9 @@ def test_obs_df(adata): dtype='float64', ) pd.testing.assert_frame_equal( - sc.get.obs_df(adata, keys=["gene2", "obs1"], obsm_keys=[("eye", 0), ("sparse", 1)]), + sc.get.obs_df( + adata, keys=["gene2", "obs1"], obsm_keys=[("eye", 0), ("sparse", 1)] + ), pd.DataFrame( {"gene2": [1, 1], "obs1": [0, 1], "eye-0": [1, 0], "sparse-1": [0.0, 1.0]}, index=adata.obs_names, @@ -355,7 +361,9 @@ def test_repeated_cols(dim, transform, func): adata = transform( sc.AnnData( np.ones((5, 10)), - obs=pd.DataFrame(np.ones((5, 2)), columns=["a_column_name", "a_column_name"]), + obs=pd.DataFrame( + np.ones((5, 2)), columns=["a_column_name", "a_column_name"] + ), var=pd.DataFrame(index=[f"gene-{i}" for i in range(10)]), ) ) @@ -372,7 +380,9 @@ def test_repeated_index_vals(dim, transform, func): adata = transform( sc.AnnData( np.ones((5, 10)), - var=pd.DataFrame(index=["repeated_id"] * 2 + [f"gene-{i}" for i in range(8)]), + var=pd.DataFrame( + index=["repeated_id"] * 2 + [f"gene-{i}" for i in range(8)] + ), ) ) diff --git a/scanpy/tests/test_highly_variable_genes.py b/scanpy/tests/test_highly_variable_genes.py index 587d895781..8b3e4f52c2 100644 --- a/scanpy/tests/test_highly_variable_genes.py +++ b/scanpy/tests/test_highly_variable_genes.py @@ -63,9 +63,13 @@ def test_higly_variable_genes_compare_to_seurat(): sc.pp.normalize_per_cell(pbmc, counts_per_cell_after=1e4) sc.pp.log1p(pbmc) - sc.pp.highly_variable_genes(pbmc, flavor='seurat', min_mean=0.0125, max_mean=3, min_disp=0.5, inplace=True) + sc.pp.highly_variable_genes( + pbmc, flavor='seurat', min_mean=0.0125, max_mean=3, min_disp=0.5, inplace=True + ) - np.testing.assert_array_equal(seurat_hvg_info['highly_variable'], pbmc.var['highly_variable']) + np.testing.assert_array_equal( + seurat_hvg_info['highly_variable'], pbmc.var['highly_variable'] + ) # (still) Not equal to tolerance rtol=2e-05, atol=2e-05 # np.testing.assert_allclose(4, 3.9999, rtol=2e-05, atol=2e-05) @@ -90,7 +94,9 @@ def test_higly_variable_genes_compare_to_seurat(): def test_higly_variable_genes_compare_to_seurat_v3(): - seurat_hvg_info = pd.read_csv(FILE_V3, sep=' ', dtype={"variances_norm": np.float64}) + seurat_hvg_info = pd.read_csv( + FILE_V3, sep=' ', dtype={"variances_norm": np.float64} + ) pbmc = sc.datasets.pbmc3k() pbmc.var_names_make_unique() @@ -101,7 +107,9 @@ def test_higly_variable_genes_compare_to_seurat_v3(): sc.pp.highly_variable_genes(pbmc, n_top_genes=1000, flavor='seurat_v3') sc.pp.highly_variable_genes(pbmc_dense, n_top_genes=1000, flavor='seurat_v3') - np.testing.assert_array_equal(seurat_hvg_info['highly_variable'], pbmc.var['highly_variable']) + np.testing.assert_array_equal( + seurat_hvg_info['highly_variable'], pbmc.var['highly_variable'] + ) np.testing.assert_allclose( seurat_hvg_info['variances'], pbmc.var['variances'], @@ -124,7 +132,9 @@ def test_higly_variable_genes_compare_to_seurat_v3(): batch = np.zeros((len(pbmc)), dtype=int) batch[1500:] = 1 pbmc.obs["batch"] = batch - df = sc.pp.highly_variable_genes(pbmc, n_top_genes=4000, flavor='seurat_v3', batch_key="batch", inplace=False) + df = sc.pp.highly_variable_genes( + pbmc, n_top_genes=4000, flavor='seurat_v3', batch_key="batch", inplace=False + ) df.sort_values( ["highly_variable_nbatches", "highly_variable_rank"], ascending=[False, True], @@ -132,7 +142,9 @@ def test_higly_variable_genes_compare_to_seurat_v3(): inplace=True, ) df = df.iloc[:4000] - seurat_hvg_info_batch = pd.read_csv(FILE_V3_BATCH, sep=' ', dtype={"variances_norm": np.float64}) + seurat_hvg_info_batch = pd.read_csv( + FILE_V3_BATCH, sep=' ', dtype={"variances_norm": np.float64} + ) # ranks might be slightly different due to many genes having same normalized var seu = pd.Index(seurat_hvg_info_batch['x'].values) @@ -164,7 +176,9 @@ def test_filter_genes_dispersion_compare_to_seurat(): min_disp=0.5, ) - np.testing.assert_array_equal(seurat_hvg_info['highly_variable'], pbmc.var['highly_variable']) + np.testing.assert_array_equal( + seurat_hvg_info['highly_variable'], pbmc.var['highly_variable'] + ) # (still) Not equal to tolerance rtol=2e-05, atol=2e-05: # np.testing.assert_allclose(4, 3.9999, rtol=2e-05, atol=2e-05) @@ -205,8 +219,12 @@ def test_highly_variable_genes_batches(): sc.pp.filter_genes(adata_1, min_cells=1) sc.pp.filter_genes(adata_2, min_cells=1) - hvg1 = sc.pp.highly_variable_genes(adata_1, flavor='cell_ranger', n_top_genes=200, inplace=False) - hvg2 = sc.pp.highly_variable_genes(adata_2, flavor='cell_ranger', n_top_genes=200, inplace=False) + hvg1 = sc.pp.highly_variable_genes( + adata_1, flavor='cell_ranger', n_top_genes=200, inplace=False + ) + hvg2 = sc.pp.highly_variable_genes( + adata_2, flavor='cell_ranger', n_top_genes=200, inplace=False + ) assert np.isclose( adata.var['dispersions_norm'][100], @@ -216,7 +234,9 @@ def test_highly_variable_genes_batches(): adata.var['dispersions_norm'][101], 0.5 * hvg1['dispersions_norm'][1] + 0.5 * hvg2['dispersions_norm'][101], ) - assert np.isclose(adata.var['dispersions_norm'][0], 0.5 * hvg2['dispersions_norm'][0]) + assert np.isclose( + adata.var['dispersions_norm'][0], 0.5 * hvg2['dispersions_norm'][0] + ) colnames = [ 'means', diff --git a/scanpy/tests/test_ingest.py b/scanpy/tests/test_ingest.py index bf3ad73605..a7ba765f98 100644 --- a/scanpy/tests/test_ingest.py +++ b/scanpy/tests/test_ingest.py @@ -137,7 +137,9 @@ def test_ingest_map_embedding_umap(): adata_ref = sc.AnnData(X) adata_new = sc.AnnData(T) - sc.pp.neighbors(adata_ref, method='umap', use_rep='X', n_neighbors=4, random_state=0) + sc.pp.neighbors( + adata_ref, method='umap', use_rep='X', n_neighbors=4, random_state=0 + ) sc.tl.umap(adata_ref, random_state=0) ing = sc.tl.Ingest(adata_ref) diff --git a/scanpy/tests/test_neighbors.py b/scanpy/tests/test_neighbors.py index 468b02fdd4..72df4fbf1a 100644 --- a/scanpy/tests/test_neighbors.py +++ b/scanpy/tests/test_neighbors.py @@ -143,9 +143,13 @@ def test_gauss_connectivities_euclidean(neigh): def test_metrics_argument(): no_knn_euclidean = get_neighbors() - no_knn_euclidean.compute_neighbors(method="gauss", knn=False, n_neighbors=n_neighbors, metric="euclidean") + no_knn_euclidean.compute_neighbors( + method="gauss", knn=False, n_neighbors=n_neighbors, metric="euclidean" + ) no_knn_manhattan = get_neighbors() - no_knn_manhattan.compute_neighbors(method="gauss", knn=False, n_neighbors=n_neighbors, metric="manhattan") + no_knn_manhattan.compute_neighbors( + method="gauss", knn=False, n_neighbors=n_neighbors, metric="manhattan" + ) assert not np.allclose(no_knn_euclidean.distances, no_knn_manhattan.distances) diff --git a/scanpy/tests/test_neighbors_key_added.py b/scanpy/tests/test_neighbors_key_added.py index db786e170c..6793a40d15 100644 --- a/scanpy/tests/test_neighbors_key_added.py +++ b/scanpy/tests/test_neighbors_key_added.py @@ -19,8 +19,12 @@ def test_neighbors_key_added(adata): dists_key = adata.uns[key]['distances_key'] assert adata.uns['neighbors']['params'] == adata.uns[key]['params'] - assert np.allclose(adata.obsp['connectivities'].toarray(), adata.obsp[conns_key].toarray()) - assert np.allclose(adata.obsp['distances'].toarray(), adata.obsp[dists_key].toarray()) + assert np.allclose( + adata.obsp['connectivities'].toarray(), adata.obsp[conns_key].toarray() + ) + assert np.allclose( + adata.obsp['distances'].toarray(), adata.obsp[dists_key].toarray() + ) # test functions with neighbors_key and obsp diff --git a/scanpy/tests/test_package_structure.py b/scanpy/tests/test_package_structure.py index bf30f7b03d..abaa8fc312 100644 --- a/scanpy/tests/test_package_structure.py +++ b/scanpy/tests/test_package_structure.py @@ -15,7 +15,9 @@ proj_dir = mod_dir.parent scanpy_functions = [ - c_or_f for c_or_f in descend_classes_and_funcs(scanpy, "scanpy") if isinstance(c_or_f, FunctionType) + c_or_f + for c_or_f in descend_classes_and_funcs(scanpy, "scanpy") + if isinstance(c_or_f, FunctionType) ] diff --git a/scanpy/tests/test_pca.py b/scanpy/tests/test_pca.py index ad7e4158f4..d4feac5562 100644 --- a/scanpy/tests/test_pca.py +++ b/scanpy/tests/test_pca.py @@ -3,7 +3,6 @@ from anndata import AnnData import scanpy as sc -from scanpy.tests.fixtures import array_type, float_dtype from anndata.tests.helpers import assert_equal A_list = [ @@ -92,7 +91,9 @@ def test_pca_sparse(pbmc3k_normalized): explicit = sc.pp.pca(pbmc_dense, dtype=np.float64, copy=True) assert np.allclose(implicit.uns["pca"]["variance"], explicit.uns["pca"]["variance"]) - assert np.allclose(implicit.uns["pca"]["variance_ratio"], explicit.uns["pca"]["variance_ratio"]) + assert np.allclose( + implicit.uns["pca"]["variance_ratio"], explicit.uns["pca"]["variance_ratio"] + ) assert np.allclose(implicit.obsm['X_pca'], explicit.obsm['X_pca']) assert np.allclose(implicit.varm['PCs'], explicit.varm['PCs']) @@ -123,9 +124,13 @@ def test_pca_chunked(pbmc3k_normalized): default = sc.pp.pca(pbmc3k_normalized, copy=True) # Taking absolute value since sometimes dimensions are flipped - np.testing.assert_allclose(np.abs(chunked.obsm["X_pca"]), np.abs(default.obsm["X_pca"])) + np.testing.assert_allclose( + np.abs(chunked.obsm["X_pca"]), np.abs(default.obsm["X_pca"]) + ) np.testing.assert_allclose(np.abs(chunked.varm["PCs"]), np.abs(default.varm["PCs"])) - np.testing.assert_allclose(np.abs(chunked.uns["pca"]["variance"]), np.abs(default.uns["pca"]["variance"])) + np.testing.assert_allclose( + np.abs(chunked.uns["pca"]["variance"]), np.abs(default.uns["pca"]["variance"]) + ) np.testing.assert_allclose( np.abs(chunked.uns["pca"]["variance_ratio"]), np.abs(default.uns["pca"]["variance_ratio"]), diff --git a/scanpy/tests/test_plotting.py b/scanpy/tests/test_plotting.py index a9eb070be1..b83d40caa3 100644 --- a/scanpy/tests/test_plotting.py +++ b/scanpy/tests/test_plotting.py @@ -134,10 +134,14 @@ def test_heatmap(image_comparer): var=pd.DataFrame({"genes": 'g1 g2 g3'.split()}).set_index('genes'), ) a.obs['foo'] = a.obs['foo'].astype('category') - sc.pl.heatmap(a, var_names=a.var_names, groupby='foo', swap_axes=True, figsize=(4, 4)) + sc.pl.heatmap( + a, var_names=a.var_names, groupby='foo', swap_axes=True, figsize=(4, 4) + ) save_and_compare_images('master_heatmap_small_swap_alignment') - sc.pl.heatmap(a, var_names=a.var_names, groupby='foo', swap_axes=False, figsize=(4, 4)) + sc.pl.heatmap( + a, var_names=a.var_names, groupby='foo', swap_axes=False, figsize=(4, 4) + ) save_and_compare_images('master_heatmap_small_alignment') @@ -161,7 +165,9 @@ def test_clustermap(image_comparer, obs_keys, name): [ ( "dotplot", - partial(sc.pl.dotplot, groupby='cell_type', title='dotplot', dendrogram=True), + partial( + sc.pl.dotplot, groupby='cell_type', title='dotplot', dendrogram=True + ), ), ( "dotplot2", @@ -415,7 +421,9 @@ def test_tracksplot(image_comparer): save_and_compare_images = image_comparer(ROOT, FIGS, tol=15) adata = sc.datasets.krumsiek11() - sc.pl.tracksplot(adata, adata.var_names, 'cell_type', dendrogram=True, use_raw=False) + sc.pl.tracksplot( + adata, adata.var_names, 'cell_type', dendrogram=True, use_raw=False + ) save_and_compare_images('master_tracksplot') @@ -552,7 +560,9 @@ def test_correlation(image_comparer): [ ( "ranked_genes_sharey", - partial(sc.pl.rank_genes_groups, n_genes=12, n_panels_per_row=3, show=False), + partial( + sc.pl.rank_genes_groups, n_genes=12, n_panels_per_row=3, show=False + ), ), ( "ranked_genes", @@ -566,7 +576,9 @@ def test_correlation(image_comparer): ), ( "ranked_genes_heatmap", - partial(sc.pl.rank_genes_groups_heatmap, n_genes=4, cmap='YlGnBu', show=False), + partial( + sc.pl.rank_genes_groups_heatmap, n_genes=4, cmap='YlGnBu', show=False + ), ), ( "ranked_genes_heatmap_swap_axes", @@ -802,7 +814,9 @@ def pbmc_scatterplots(): pytest.param( 'tsne', partial(sc.pl.tsne, color=['CD3D', 'louvain']), - marks=pytest.mark.xfail(reason='slight differences even after setting random_state.'), + marks=pytest.mark.xfail( + reason='slight differences even after setting random_state.' + ), ), ('umap_nocolor', sc.pl.umap), ( @@ -1049,7 +1063,10 @@ def test_scatter_rep(tmpdir): ), columns=["rep", "gene", "result"], ) - states["outpth"] = [TESTDIR / f"{state.gene}_{state.rep}_{state.result}.png" for state in states.itertuples()] + states["outpth"] = [ + TESTDIR / f"{state.gene}_{state.rep}_{state.result}.png" + for state in states.itertuples() + ] pattern = np.array(list(chain.from_iterable(repeat(i, 5) for i in range(3)))) coords = np.c_[np.arange(15) % 5, pattern] @@ -1112,7 +1129,10 @@ def test_paga(image_comparer): sc.pl.paga_compare(pbmc, basis='X_pca', legend_fontweight='normal', **common) save_and_compare_images('master_paga_compare_pca') - colors = {c: {cm.Set1(_): 0.33 for _ in range(3)} for c in pbmc.obs["bulk_labels"].cat.categories} + colors = { + c: {cm.Set1(_): 0.33 for _ in range(3)} + for c in pbmc.obs["bulk_labels"].cat.categories + } colors["Dendritic"] = {cm.Set2(_): 0.25 for _ in range(4)} sc.pl.paga(pbmc, color=colors, colorbar=False) diff --git a/scanpy/tests/test_preprocessing.py b/scanpy/tests/test_preprocessing.py index c2167c4796..b9decb0579 100644 --- a/scanpy/tests/test_preprocessing.py +++ b/scanpy/tests/test_preprocessing.py @@ -39,7 +39,9 @@ def base(request): def test_log1p_rep(count_matrix_format, base, dtype): - X = count_matrix_format(np.abs(sp.random(100, 200, density=0.3, dtype=dtype)).toarray()) + X = count_matrix_format( + np.abs(sp.random(100, 200, density=0.3, dtype=dtype)).toarray() + ) check_rep_mutation(sc.pp.log1p, X, base=base) check_rep_results(sc.pp.log1p, X, base=base) @@ -167,11 +169,15 @@ def test_regress_out_ordinal(): adata.obs['n_counts'] = adata.X.sum(axis=1) # results using only one processor - single = sc.pp.regress_out(adata, keys=['n_counts', 'percent_mito'], n_jobs=1, copy=True) + single = sc.pp.regress_out( + adata, keys=['n_counts', 'percent_mito'], n_jobs=1, copy=True + ) assert adata.X.shape == single.X.shape # results using 8 processors - multi = sc.pp.regress_out(adata, keys=['n_counts', 'percent_mito'], n_jobs=8, copy=True) + multi = sc.pp.regress_out( + adata, keys=['n_counts', 'percent_mito'], n_jobs=8, copy=True + ) np.testing.assert_array_equal(single.X, multi.X) @@ -249,11 +255,15 @@ def test_downsample_counts_per_cell(count_matrix_format, replace, dtype): X = X.astype(dtype) adata = AnnData(X=count_matrix_format(X), dtype=dtype) with pytest.raises(ValueError): - sc.pp.downsample_counts(adata, counts_per_cell=TARGET, total_counts=TARGET, replace=replace) + sc.pp.downsample_counts( + adata, counts_per_cell=TARGET, total_counts=TARGET, replace=replace + ) with pytest.raises(ValueError): sc.pp.downsample_counts(adata, replace=replace) initial_totals = np.ravel(adata.X.sum(axis=1)) - adata = sc.pp.downsample_counts(adata, counts_per_cell=TARGET, replace=replace, copy=True) + adata = sc.pp.downsample_counts( + adata, counts_per_cell=TARGET, replace=replace, copy=True + ) new_totals = np.ravel(adata.X.sum(axis=1)) if sp.issparse(adata.X): assert all(adata.X.toarray()[X == 0] == 0) @@ -261,13 +271,17 @@ def test_downsample_counts_per_cell(count_matrix_format, replace, dtype): assert all(adata.X[X == 0] == 0) assert all(new_totals <= TARGET) assert all(initial_totals >= new_totals) - assert all(initial_totals[initial_totals <= TARGET] == new_totals[initial_totals <= TARGET]) + assert all( + initial_totals[initial_totals <= TARGET] == new_totals[initial_totals <= TARGET] + ) if not replace: assert np.all(X >= adata.X) assert X.dtype == adata.X.dtype -def test_downsample_counts_per_cell_multiple_targets(count_matrix_format, replace, dtype): +def test_downsample_counts_per_cell_multiple_targets( + count_matrix_format, replace, dtype +): TARGETS = np.random.randint(500, 1500, 1000) X = np.random.randint(0, 100, (1000, 100)) * np.random.binomial(1, 0.3, (1000, 100)) X = X.astype(dtype) @@ -275,7 +289,9 @@ def test_downsample_counts_per_cell_multiple_targets(count_matrix_format, replac initial_totals = np.ravel(adata.X.sum(axis=1)) with pytest.raises(ValueError): sc.pp.downsample_counts(adata, counts_per_cell=[40, 10], replace=replace) - adata = sc.pp.downsample_counts(adata, counts_per_cell=TARGETS, replace=replace, copy=True) + adata = sc.pp.downsample_counts( + adata, counts_per_cell=TARGETS, replace=replace, copy=True + ) new_totals = np.ravel(adata.X.sum(axis=1)) if sp.issparse(adata.X): assert all(adata.X.toarray()[X == 0] == 0) @@ -283,7 +299,10 @@ def test_downsample_counts_per_cell_multiple_targets(count_matrix_format, replac assert all(adata.X[X == 0] == 0) assert all(new_totals <= TARGETS) assert all(initial_totals >= new_totals) - assert all(initial_totals[initial_totals <= TARGETS] == new_totals[initial_totals <= TARGETS]) + assert all( + initial_totals[initial_totals <= TARGETS] + == new_totals[initial_totals <= TARGETS] + ) if not replace: assert np.all(X >= adata.X) assert X.dtype == adata.X.dtype @@ -296,7 +315,9 @@ def test_downsample_total_counts(count_matrix_format, replace, dtype): total = X.sum() target = np.floor_divide(total, 10) initial_totals = np.ravel(adata_orig.X.sum(axis=1)) - adata = sc.pp.downsample_counts(adata_orig, total_counts=target, replace=replace, copy=True) + adata = sc.pp.downsample_counts( + adata_orig, total_counts=target, replace=replace, copy=True + ) new_totals = np.ravel(adata.X.sum(axis=1)) if sp.issparse(adata.X): assert all(adata.X.toarray()[X == 0] == 0) @@ -306,7 +327,9 @@ def test_downsample_total_counts(count_matrix_format, replace, dtype): assert all(initial_totals >= new_totals) if not replace: assert np.all(X >= adata.X) - adata = sc.pp.downsample_counts(adata_orig, total_counts=total + 10, replace=False, copy=True) + adata = sc.pp.downsample_counts( + adata_orig, total_counts=total + 10, replace=False, copy=True + ) assert (adata.X == X).all() assert X.dtype == adata.X.dtype diff --git a/scanpy/tests/test_preprocessing_distributed.py b/scanpy/tests/test_preprocessing_distributed.py index a24540a75f..871656c1ce 100644 --- a/scanpy/tests/test_preprocessing_distributed.py +++ b/scanpy/tests/test_preprocessing_distributed.py @@ -16,7 +16,9 @@ installed = {mod: bool(find_spec(mod)) for mod in required} -@pytest.mark.skipif(not all(installed.values()), reason=f'{required} all required: {installed}') +@pytest.mark.skipif( + not all(installed.values()), reason=f'{required} all required: {installed}' +) class TestPreprocessingDistributed: @pytest.fixture() def adata(self): diff --git a/scanpy/tests/test_qc_metrics.py b/scanpy/tests/test_qc_metrics.py index 33869de5a1..71f6e728e0 100644 --- a/scanpy/tests/test_qc_metrics.py +++ b/scanpy/tests/test_qc_metrics.py @@ -50,7 +50,9 @@ def test_segments_binary(): assert (segfull == propfull).all() -@pytest.mark.parametrize("cls", [np.asarray, sparse.csr_matrix, sparse.csc_matrix, sparse.coo_matrix]) +@pytest.mark.parametrize( + "cls", [np.asarray, sparse.csr_matrix, sparse.csc_matrix, sparse.coo_matrix] +) def test_top_segments(cls): a = cls(np.ones((300, 100))) seg = top_segment_proportions(a, [50, 100]) @@ -65,16 +67,25 @@ def test_top_segments(cls): # they’re also just making sure the metrics are there def test_qc_metrics(): adata = AnnData(X=sparse.csr_matrix(np.random.binomial(100, 0.005, (1000, 1000)))) - adata.var["mito"] = np.concatenate((np.ones(100, dtype=bool), np.zeros(900, dtype=bool))) + adata.var["mito"] = np.concatenate( + (np.ones(100, dtype=bool), np.zeros(900, dtype=bool)) + ) adata.var["negative"] = False sc.pp.calculate_qc_metrics(adata, qc_vars=["mito", "negative"], inplace=True) assert (adata.obs["n_genes_by_counts"] < adata.shape[1]).all() - assert (adata.obs["n_genes_by_counts"] >= adata.obs["log1p_n_genes_by_counts"]).all() + assert ( + adata.obs["n_genes_by_counts"] >= adata.obs["log1p_n_genes_by_counts"] + ).all() assert (adata.obs["total_counts"] == np.ravel(adata.X.sum(axis=1))).all() assert (adata.obs["total_counts"] >= adata.obs["log1p_total_counts"]).all() - assert (adata.obs["total_counts_mito"] >= adata.obs["log1p_total_counts_mito"]).all() + assert ( + adata.obs["total_counts_mito"] >= adata.obs["log1p_total_counts_mito"] + ).all() assert (adata.obs["total_counts_negative"] == 0).all() - assert (adata.obs["pct_counts_in_top_50_genes"] <= adata.obs["pct_counts_in_top_100_genes"]).all() + assert ( + adata.obs["pct_counts_in_top_50_genes"] + <= adata.obs["pct_counts_in_top_100_genes"] + ).all() for col in filter(lambda x: "negative" not in x, adata.obs.columns): assert (adata.obs[col] >= 0).all() # Values should be positive or zero assert (adata.obs[col] != 0).any().all() # Nothing should be all zeros @@ -97,21 +108,29 @@ def test_qc_metrics(): assert np.allclose(adata.var[col], old_var[col]) # with log1p=False adata = AnnData(X=sparse.csr_matrix(np.random.binomial(100, 0.005, (1000, 1000)))) - adata.var["mito"] = np.concatenate((np.ones(100, dtype=bool), np.zeros(900, dtype=bool))) + adata.var["mito"] = np.concatenate( + (np.ones(100, dtype=bool), np.zeros(900, dtype=bool)) + ) adata.var["negative"] = False - sc.pp.calculate_qc_metrics(adata, qc_vars=["mito", "negative"], log1p=False, inplace=True) + sc.pp.calculate_qc_metrics( + adata, qc_vars=["mito", "negative"], log1p=False, inplace=True + ) assert not np.any(adata.obs.columns.str.startswith("log1p_")) assert not np.any(adata.var.columns.str.startswith("log1p_")) def adata_mito(): a = np.random.binomial(100, 0.005, (1000, 1000)) - init_var = pd.DataFrame(dict(mito=np.concatenate((np.ones(100, dtype=bool), np.zeros(900, dtype=bool))))) + init_var = pd.DataFrame( + dict(mito=np.concatenate((np.ones(100, dtype=bool), np.zeros(900, dtype=bool)))) + ) adata_dense = AnnData(X=a, var=init_var.copy()) return adata_dense, init_var -@pytest.mark.parametrize("cls", [np.asarray, sparse.csr_matrix, sparse.csc_matrix, sparse.coo_matrix]) +@pytest.mark.parametrize( + "cls", [np.asarray, sparse.csr_matrix, sparse.csc_matrix, sparse.coo_matrix] +) def test_qc_metrics_format(cls): adata_dense, init_var = adata_mito() sc.pp.calculate_qc_metrics(adata_dense, qc_vars=["mito"], inplace=True) diff --git a/scanpy/tests/test_queries.py b/scanpy/tests/test_queries.py index c97e2e6767..e4ef9cc69d 100644 --- a/scanpy/tests/test_queries.py +++ b/scanpy/tests/test_queries.py @@ -20,7 +20,9 @@ def test_enrich(): sc.queries.enrich(pbmc, "1") gene_dict = {'set1': ['KLF4', 'PAX5'], 'set2': ['SOX2', 'NANOG']} - enrich_list = sc.queries.enrich(gene_dict, org="hsapiens", gprofiler_kwargs=dict(sources=['GO:BP'])) + enrich_list = sc.queries.enrich( + gene_dict, org="hsapiens", gprofiler_kwargs=dict(sources=['GO:BP']) + ) assert 'set1' in enrich_list['query'].unique() assert 'set2' in enrich_list['query'].unique() @@ -29,4 +31,6 @@ def test_enrich(): def test_mito_genes(): pbmc = sc.datasets.pbmc68k_reduced() mt_genes = sc.queries.mitochondrial_genes("hsapiens") - assert pbmc.var_names.isin(mt_genes["external_gene_name"]).sum() == 1 # Should only be MT-ND3 + assert ( + pbmc.var_names.isin(mt_genes["external_gene_name"]).sum() == 1 + ) # Should only be MT-ND3 diff --git a/scanpy/tests/test_rank_genes_groups.py b/scanpy/tests/test_rank_genes_groups.py index 269adb4990..1592a5c6a8 100644 --- a/scanpy/tests/test_rank_genes_groups.py +++ b/scanpy/tests/test_rank_genes_groups.py @@ -28,9 +28,13 @@ def get_example_data(*, sparse=False): # create test object - adata = AnnData(np.multiply(binomial(1, 0.15, (100, 20)), negative_binomial(2, 0.25, (100, 20)))) + adata = AnnData( + np.multiply(binomial(1, 0.15, (100, 20)), negative_binomial(2, 0.25, (100, 20))) + ) # adapt marker_genes for cluster (so as to have some form of reasonable input - adata.X[0:10, 0:5] = np.multiply(binomial(1, 0.9, (10, 5)), negative_binomial(1, 0.5, (10, 5))) + adata.X[0:10, 0:5] = np.multiply( + binomial(1, 0.9, (10, 5)), negative_binomial(1, 0.5, (10, 5)) + ) # The following construction is inefficient, but makes sure that the same data is used in the sparse case if sparse: @@ -77,22 +81,30 @@ def test_results_dense(): rank_genes_groups(adata, 'true_groups', n_genes=20, method='t-test') - adata.uns['rank_genes_groups']['names'] = adata.uns['rank_genes_groups']['names'].astype(true_names_t_test.dtype) + adata.uns['rank_genes_groups']['names'] = adata.uns['rank_genes_groups'][ + 'names' + ].astype(true_names_t_test.dtype) for name in true_scores_t_test.dtype.names: - assert np.allclose(true_scores_t_test[name], adata.uns['rank_genes_groups']['scores'][name]) + assert np.allclose( + true_scores_t_test[name], adata.uns['rank_genes_groups']['scores'][name] + ) assert np.array_equal(true_names_t_test, adata.uns['rank_genes_groups']['names']) rank_genes_groups(adata, 'true_groups', n_genes=20, method='wilcoxon') - adata.uns['rank_genes_groups']['names'] = adata.uns['rank_genes_groups']['names'].astype(true_names_wilcoxon.dtype) + adata.uns['rank_genes_groups']['names'] = adata.uns['rank_genes_groups'][ + 'names' + ].astype(true_names_wilcoxon.dtype) for name in true_scores_t_test.dtype.names: assert np.allclose( true_scores_wilcoxon[name][:7], adata.uns['rank_genes_groups']['scores'][name][:7], ) - assert np.array_equal(true_names_wilcoxon[:7], adata.uns['rank_genes_groups']['names'][:7]) + assert np.array_equal( + true_names_wilcoxon[:7], adata.uns['rank_genes_groups']['names'][:7] + ) def test_results_sparse(): @@ -109,22 +121,30 @@ def test_results_sparse(): rank_genes_groups(adata, 'true_groups', n_genes=20, method='t-test') - adata.uns['rank_genes_groups']['names'] = adata.uns['rank_genes_groups']['names'].astype(true_names_t_test.dtype) + adata.uns['rank_genes_groups']['names'] = adata.uns['rank_genes_groups'][ + 'names' + ].astype(true_names_t_test.dtype) for name in true_scores_t_test.dtype.names: - assert np.allclose(true_scores_t_test[name], adata.uns['rank_genes_groups']['scores'][name]) + assert np.allclose( + true_scores_t_test[name], adata.uns['rank_genes_groups']['scores'][name] + ) assert np.array_equal(true_names_t_test, adata.uns['rank_genes_groups']['names']) rank_genes_groups(adata, 'true_groups', n_genes=20, method='wilcoxon') - adata.uns['rank_genes_groups']['names'] = adata.uns['rank_genes_groups']['names'].astype(true_names_wilcoxon.dtype) + adata.uns['rank_genes_groups']['names'] = adata.uns['rank_genes_groups'][ + 'names' + ].astype(true_names_wilcoxon.dtype) for name in true_scores_t_test.dtype.names: assert np.allclose( true_scores_wilcoxon[name][:7], adata.uns['rank_genes_groups']['scores'][name][:7], ) - assert np.array_equal(true_names_wilcoxon[:7], adata.uns['rank_genes_groups']['names'][:7]) + assert np.array_equal( + true_names_wilcoxon[:7], adata.uns['rank_genes_groups']['names'][:7] + ) def test_results_layers(): @@ -215,7 +235,11 @@ def test_wilcoxon_symmetry(): rankby_abs=True, ) - stats_mono = rank_genes_groups_df(pbmc, group="CD14+ Monocyte").drop(columns="names").to_numpy() + stats_mono = ( + rank_genes_groups_df(pbmc, group="CD14+ Monocyte") + .drop(columns="names") + .to_numpy() + ) rank_genes_groups( pbmc, @@ -226,7 +250,9 @@ def test_wilcoxon_symmetry(): rankby_abs=True, ) - stats_dend = rank_genes_groups_df(pbmc, group="Dendritic").drop(columns="names").to_numpy() + stats_dend = ( + rank_genes_groups_df(pbmc, group="Dendritic").drop(columns="names").to_numpy() + ) assert np.allclose(np.abs(stats_mono), np.abs(stats_dend)) diff --git a/scanpy/tests/test_rank_genes_groups_logreg.py b/scanpy/tests/test_rank_genes_groups_logreg.py index a13997458e..f64a3c3fb7 100644 --- a/scanpy/tests/test_rank_genes_groups_logreg.py +++ b/scanpy/tests/test_rank_genes_groups_logreg.py @@ -34,7 +34,9 @@ def test_rank_genes_groups_with_renamed_categories_use_rep(): adata.X = adata.X[::-1, :] sc.tl.louvain(adata) - sc.tl.rank_genes_groups(adata, 'louvain', method='logreg', layer="to_test", use_raw=False) + sc.tl.rank_genes_groups( + adata, 'louvain', method='logreg', layer="to_test", use_raw=False + ) assert adata.uns['rank_genes_groups']['names'].dtype.names == ('0', '1', '2') assert adata.uns['rank_genes_groups']['names'][0].tolist() == ('3', '1', '0') diff --git a/scanpy/tests/test_read_10x.py b/scanpy/tests/test_read_10x.py index 0ee8d7806c..6ed125ab4c 100644 --- a/scanpy/tests/test_read_10x.py +++ b/scanpy/tests/test_read_10x.py @@ -67,7 +67,9 @@ def test_read_10x_h5_v1(): ROOT / '1.2.0' / 'filtered_gene_bc_matrices_h5.h5', genome='hg19_chr21', ) - nospec_genome_v1 = sc.read_10x_h5(ROOT / '1.2.0' / 'filtered_gene_bc_matrices_h5.h5') + nospec_genome_v1 = sc.read_10x_h5( + ROOT / '1.2.0' / 'filtered_gene_bc_matrices_h5.h5' + ) assert_anndata_equal(spec_genome_v1, nospec_genome_v1) @@ -111,4 +113,6 @@ def test_read_visium_counts(): def test_10x_h5_gex(): # Tests that gex option doesn't, say, make the function return None h5_pth = ROOT / '3.0.0' / 'filtered_feature_bc_matrix.h5' - assert_anndata_equal(sc.read_10x_h5(h5_pth, gex_only=True), sc.read_10x_h5(h5_pth, gex_only=False)) + assert_anndata_equal( + sc.read_10x_h5(h5_pth, gex_only=True), sc.read_10x_h5(h5_pth, gex_only=False) + ) diff --git a/scanpy/tests/test_score_genes.py b/scanpy/tests/test_score_genes.py index 497a72e0fc..9500031f66 100644 --- a/scanpy/tests/test_score_genes.py +++ b/scanpy/tests/test_score_genes.py @@ -9,7 +9,12 @@ def _create_random_gene_names(n_genes, name_length): """ creates a bunch of random gene names (just CAPS letters) """ - return np.array([''.join(map(chr, np.random.randint(65, 90, name_length))) for _ in range(n_genes)]) + return np.array( + [ + ''.join(map(chr, np.random.randint(65, 90, name_length))) + for _ in range(n_genes) + ] + ) def _create_sparse_nan_matrix(rows, cols, percent_zero, percent_nan): @@ -52,7 +57,9 @@ def test_add_score(): # the actual genes names are all 6letters # create some non-estinsting names with 7 letters: non_existing_genes = _create_random_gene_names(n_genes=3, name_length=7) - some_genes = np.r_[np.unique(np.random.choice(adata.var_names, 10)), np.unique(non_existing_genes)] + some_genes = np.r_[ + np.unique(np.random.choice(adata.var_names, 10)), np.unique(non_existing_genes) + ] sc.tl.score_genes(adata, some_genes, score_name='Test') assert adata.obs['Test'].dtype == 'float32' @@ -73,8 +80,12 @@ def test_sparse_nanmean(): # sparse matrix with nan S = _create_sparse_nan_matrix(R, C, percent_zero=0.3, percent_nan=0.3) - np.testing.assert_allclose(np.nanmean(S.A, 1), np.array(_sparse_nanmean(S, 1)).flatten()) - np.testing.assert_allclose(np.nanmean(S.A, 0), np.array(_sparse_nanmean(S, 0)).flatten()) + np.testing.assert_allclose( + np.nanmean(S.A, 1), np.array(_sparse_nanmean(S, 1)).flatten() + ) + np.testing.assert_allclose( + np.nanmean(S.A, 0), np.array(_sparse_nanmean(S, 0)).flatten() + ) # edge case of only NaNs per row A = np.full((10, 1), np.nan) @@ -107,7 +118,9 @@ def test_score_genes_sparse_vs_dense(): sc.tl.score_genes(adata_sparse, gene_list=gene_set, score_name='Test') sc.tl.score_genes(adata_dense, gene_list=gene_set, score_name='Test') - np.testing.assert_allclose(adata_sparse.obs['Test'].values, adata_dense.obs['Test'].values) + np.testing.assert_allclose( + adata_sparse.obs['Test'].values, adata_dense.obs['Test'].values + ) def test_score_genes_deplete(): diff --git a/scanpy/tools/_dendrogram.py b/scanpy/tools/_dendrogram.py index 7bcd67328c..788db127a6 100644 --- a/scanpy/tools/_dendrogram.py +++ b/scanpy/tools/_dendrogram.py @@ -109,12 +109,16 @@ def dendrogram( ) if var_names is None: - rep_df = pd.DataFrame(_choose_representation(adata, use_rep=use_rep, n_pcs=n_pcs)) + rep_df = pd.DataFrame( + _choose_representation(adata, use_rep=use_rep, n_pcs=n_pcs) + ) categorical = adata.obs[groupby[0]] if len(groupby) > 1: for group in groupby[1:]: # create new category by merging the given groupby categories - categorical = (categorical.astype(str) + "_" + adata.obs[group].astype(str)).astype('category') + categorical = ( + categorical.astype(str) + "_" + adata.obs[group].astype(str) + ).astype('category') categorical.name = "_".join(groupby) rep_df.set_index(categorical, inplace=True) @@ -133,7 +137,9 @@ def dendrogram( corr_matrix = mean_df.T.corr(method=cor_method) corr_condensed = distance.squareform(1 - corr_matrix) - z_var = sch.linkage(corr_condensed, method=linkage_method, optimal_ordering=optimal_ordering) + z_var = sch.linkage( + corr_condensed, method=linkage_method, optimal_ordering=optimal_ordering + ) dendro_info = sch.dendrogram(z_var, labels=list(categories), no_plot=True) dat = dict( diff --git a/scanpy/tools/_diffmap.py b/scanpy/tools/_diffmap.py index a04e3b4a1a..e5b5b8b8f4 100644 --- a/scanpy/tools/_diffmap.py +++ b/scanpy/tools/_diffmap.py @@ -64,7 +64,9 @@ def diffmap( neighbors_key = 'neighbors' if neighbors_key not in adata.uns: - raise ValueError('You need to run `pp.neighbors` first to compute a neighborhood graph.') + raise ValueError( + 'You need to run `pp.neighbors` first to compute a neighborhood graph.' + ) if n_comps <= 2: raise ValueError('Provide any value greater than 2 for `n_comps`. ') adata = adata.copy() if copy else adata diff --git a/scanpy/tools/_dpt.py b/scanpy/tools/_dpt.py index ae191bfa77..35937dc7f0 100644 --- a/scanpy/tools/_dpt.py +++ b/scanpy/tools/_dpt.py @@ -148,7 +148,9 @@ def dpt( logg.info(' this uses a hierarchical implementation') if dpt.iroot is not None: dpt._set_pseudotime() # pseudotimes are distances from root point - adata.uns['iroot'] = dpt.iroot # update iroot, might have changed when subsampling, for example + adata.uns[ + 'iroot' + ] = dpt.iroot # update iroot, might have changed when subsampling, for example adata.obs['dpt_pseudotime'] = dpt.pseudotime # detect branchings and partition the data into segments if n_branchings > 0: @@ -172,7 +174,11 @@ def dpt( time=start, deep=( 'added\n' - + (" 'dpt_pseudotime', the pseudotime (adata.obs)" if dpt.iroot is not None else '') + + ( + " 'dpt_pseudotime', the pseudotime (adata.obs)" + if dpt.iroot is not None + else '' + ) + ( "\n 'dpt_groups', the branching subgroups of dpt (adata.obs)" "\n 'dpt_order', cell order (adata.obs)" @@ -201,7 +207,11 @@ def __init__( super().__init__(adata, n_dcs=n_dcs, neighbors_key=neighbors_key) self.flavor = 'haghverdi16' self.n_branchings = n_branchings - self.min_group_size = min_group_size if min_group_size >= 1 else int(min_group_size * self._adata.shape[0]) + self.min_group_size = ( + min_group_size + if min_group_size >= 1 + else int(min_group_size * self._adata.shape[0]) + ) self.passed_adata = adata # just for debugging purposes self.choose_largest_segment = False self.allow_kendall_tau_shift = allow_kendall_tau_shift @@ -241,7 +251,8 @@ def detect_branchings(self): List of indices of the tips of segments. """ logg.debug( - f' detect {self.n_branchings} ' f'branching{"" if self.n_branchings == 1 else "s"}', + f' detect {self.n_branchings} ' + f'branching{"" if self.n_branchings == 1 else "s"}', ) # a segment is a subset of points of the data set (defined by the # indices of the points in the segment) @@ -271,7 +282,11 @@ def detect_branchings(self): # # let us define the tips of the whole data set if False: # this is safe, but not compatible with on-the-fly computation - tips_all = np.array(np.unravel_index(np.argmax(self.distances_dpt), self.distances_dpt.shape)) + tips_all = np.array( + np.unravel_index( + np.argmax(self.distances_dpt), self.distances_dpt.shape + ) + ) else: if self.iroot is not None: tip_0 = np.argmax(self.distances_dpt[self.iroot]) @@ -283,7 +298,10 @@ def detect_branchings(self): segs_connects = [[]] segs_undecided = [True] segs_adjacency = [[]] - logg.debug(' do not consider groups with less than ' f'{self.min_group_size} points for splitting') + logg.debug( + ' do not consider groups with less than ' + f'{self.min_group_size} points for splitting' + ) for ibranch in range(self.n_branchings): iseg, tips3 = self.select_segment(segs, segs_tips, segs_undecided) if iseg == -1: @@ -313,7 +331,9 @@ def detect_branchings(self): self.segs_connects[i, seg_adjacency] = segs_connects[i] for i in range(len(segs)): for j in range(len(segs)): - self.segs_adjacency[i, j] = self.distances_dpt[self.segs_connects[i, j], self.segs_connects[j, i]] + self.segs_adjacency[i, j] = self.distances_dpt[ + self.segs_connects[i, j], self.segs_connects[j, i] + ] self.segs_adjacency = self.segs_adjacency.tocsr() self.segs_connects = self.segs_connects.tocsr() @@ -324,11 +344,13 @@ def check_adjacency(self): if n_edges_per_seg[iseg] == n_edges: _ = self.segs_adjacency[iseg].todense().A1 closest_points_other_segs = [ - seg[np.argmin(self.distances_dpt[self.segs_tips[iseg][0], seg])] for seg in self.segs + seg[np.argmin(self.distances_dpt[self.segs_tips[iseg][0], seg])] + for seg in self.segs ] seg = self.segs[iseg] closest_points_in_segs = [ - seg[np.argmin(self.distances_dpt[tips[0], seg])] for tips in self.segs_tips + seg[np.argmin(self.distances_dpt[tips[0], seg])] + for tips in self.segs_tips ] distance_segs = [ self.distances_dpt[closest_points_other_segs[ipoint], point] @@ -381,8 +403,13 @@ def select_segment(self, segs, segs_tips, segs_undecided) -> Tuple[int, int]: # take the inner tip, the "second tip" of the segment for itip in range(2): if ( - self.distances_dpt[segs_tips[jseg][1], segs_tips[iseg][itip]] - < 0.5 * self.distances_dpt[segs_tips[iseg][~itip], segs_tips[iseg][itip]] + self.distances_dpt[ + segs_tips[jseg][1], segs_tips[iseg][itip] + ] + < 0.5 + * self.distances_dpt[ + segs_tips[iseg][~itip], segs_tips[iseg][itip] + ] ): # logg.debug( # ' group', iseg, 'with tip', segs_tips[iseg][itip], @@ -416,7 +443,9 @@ def select_segment(self, segs, segs_tips, segs_undecided) -> Tuple[int, int]: # if we did not normalize, there would be a danger of simply # assigning the highest score to the longest segment score = dseg[tips3[2]] / Dseg[tips3[0], tips3[1]] - score = len(seg) if self.choose_largest_segment else score # simply the number of points + score = ( + len(seg) if self.choose_largest_segment else score + ) # simply the number of points logg.debug( f' group {iseg} score {score} n_points {len(seg)} ' + '(too small)' if len(seg) < self.min_group_size @@ -547,7 +576,9 @@ def detect_branching( segs_tips.insert(iseg, ssegs_tips[trunk]) # append other segments segs += [seg for iseg, seg in enumerate(ssegs) if iseg != trunk] - segs_tips += [seg_tips for iseg, seg_tips in enumerate(ssegs_tips) if iseg != trunk] + segs_tips += [ + seg_tips for iseg, seg_tips in enumerate(ssegs_tips) if iseg != trunk + ] if len(ssegs) == 4: # insert undecided cells at same position segs_undecided.pop(iseg) @@ -579,7 +610,9 @@ def detect_branching( segs_connects[kseg].append(idx) break iseg_cnt += 1 - segs_adjacency[iseg] += list(range(len(segs_adjacency) - n_add, len(segs_adjacency))) + segs_adjacency[iseg] += list( + range(len(segs_adjacency) - n_add, len(segs_adjacency)) + ) segs_connects[iseg] += ssegs_connects[trunk] else: import networkx as nx @@ -595,14 +628,28 @@ def detect_branching( for kseg in kseg_list: reference_point_in_k = segs_tips[kseg][0] closest_points_in_jseg.append( - segs[jseg][np.argmin(self.distances_dpt[reference_point_in_k, segs[jseg]])] + segs[jseg][ + np.argmin( + self.distances_dpt[reference_point_in_k, segs[jseg]] + ) + ] ) # do not use the tip in the large segment j, instead, use the closest point - reference_point_in_j = closest_points_in_jseg[-1] # segs_tips[jseg][0] + reference_point_in_j = closest_points_in_jseg[ + -1 + ] # segs_tips[jseg][0] closest_points_in_kseg.append( - segs[kseg][np.argmin(self.distances_dpt[reference_point_in_j, segs[kseg]])] + segs[kseg][ + np.argmin( + self.distances_dpt[reference_point_in_j, segs[kseg]] + ) + ] + ) + distances.append( + self.distances_dpt[ + closest_points_in_jseg[-1], closest_points_in_kseg[-1] + ] ) - distances.append(self.distances_dpt[closest_points_in_jseg[-1], closest_points_in_kseg[-1]]) # print(jseg, '(', segs_tips[jseg][0], closest_points_in_jseg[-1], ')', # kseg, '(', segs_tips[kseg][0], closest_points_in_kseg[-1], ') :', distances[-1]) idx = np.argmin(distances) @@ -623,22 +670,42 @@ def detect_branching( distances = [] closest_points_in_jseg = [] closest_points_in_kseg = [] - jseg_list = [jseg for jseg in range(len(segs)) if jseg != kseg and jseg not in prev_connecting_segments] + jseg_list = [ + jseg + for jseg in range(len(segs)) + if jseg != kseg and jseg not in prev_connecting_segments + ] for jseg in jseg_list: reference_point_in_k = segs_tips[kseg][0] closest_points_in_jseg.append( - segs[jseg][np.argmin(self.distances_dpt[reference_point_in_k, segs[jseg]])] + segs[jseg][ + np.argmin( + self.distances_dpt[reference_point_in_k, segs[jseg]] + ) + ] ) # do not use the tip in the large segment j, instead, use the closest point - reference_point_in_j = closest_points_in_jseg[-1] # segs_tips[jseg][0] + reference_point_in_j = closest_points_in_jseg[ + -1 + ] # segs_tips[jseg][0] closest_points_in_kseg.append( - segs[kseg][np.argmin(self.distances_dpt[reference_point_in_j, segs[kseg]])] + segs[kseg][ + np.argmin( + self.distances_dpt[reference_point_in_j, segs[kseg]] + ) + ] + ) + distances.append( + self.distances_dpt[ + closest_points_in_jseg[-1], closest_points_in_kseg[-1] + ] ) - distances.append(self.distances_dpt[closest_points_in_jseg[-1], closest_points_in_kseg[-1]]) idx = np.argmin(distances) jseg_min = jseg_list[idx] if jseg_min not in kseg_list: - segs_adjacency_sparse = sp.sparse.lil_matrix((len(segs), len(segs)), dtype=float) + segs_adjacency_sparse = sp.sparse.lil_matrix( + (len(segs), len(segs)), dtype=float + ) for i, seg_adjacency in enumerate(segs_adjacency): segs_adjacency_sparse[i, seg_adjacency] = 1 G = nx.Graph(segs_adjacency_sparse) @@ -652,7 +719,10 @@ def detect_branching( # if we split the cluster, we should not attach kseg do_not_attach_kseg = True else: - logg.debug(f' cannot attach new segment {kseg} at {jseg_min} ' '(would produce cycle)') + logg.debug( + f' cannot attach new segment {kseg} at {jseg_min} ' + '(would produce cycle)' + ) if kseg != kseg_list[-1]: logg.debug(' continue') continue @@ -711,7 +781,9 @@ def _detect_branching( elif self.flavor == 'wolf17_bi' or self.flavor == 'wolf17_bi_un': ssegs = self._detect_branching_single_wolf17_bi(Dseg, tips) else: - raise ValueError('`flavor` needs to be in {"haghverdi16", "wolf17_tri", "wolf17_bi"}.') + raise ValueError( + '`flavor` needs to be in {"haghverdi16", "wolf17_tri", "wolf17_bi"}.' + ) # make sure that each data point has a unique association with a segment masks = np.zeros((len(ssegs), Dseg.shape[0]), dtype=bool) for iseg, seg in enumerate(ssegs): @@ -736,13 +808,19 @@ def _detect_branching( for inewseg, newseg_tips in enumerate(ssegs_tips): reference_point = newseg_tips[0] # closest cell to the new segment within undecided cells - closest_cell = undecided_cells[np.argmin(Dseg[reference_point][undecided_cells])] + closest_cell = undecided_cells[ + np.argmin(Dseg[reference_point][undecided_cells]) + ] ssegs_connects[inewseg].append(closest_cell) # closest cell to the undecided cells within new segment - closest_cell = ssegs[inewseg][np.argmin(Dseg[closest_cell][ssegs[inewseg]])] + closest_cell = ssegs[inewseg][ + np.argmin(Dseg[closest_cell][ssegs[inewseg]]) + ] ssegs_connects[-1].append(closest_cell) # also compute tips for the undecided cells - tip_0 = undecided_cells[np.argmax(Dseg[undecided_cells[0]][undecided_cells])] + tip_0 = undecided_cells[ + np.argmax(Dseg[undecided_cells[0]][undecided_cells]) + ] tip_1 = undecided_cells[np.argmax(Dseg[tip_0][undecided_cells])] ssegs_tips.append([tip_0, tip_1]) ssegs_adjacency = [[3], [3], [3], [0, 1, 2]] @@ -756,35 +834,59 @@ def _detect_branching( # this is another strategy than for the undecided_cells # here it's possible to use the more symmetric procedure # shouldn't make much of a difference - closest_points[0, 1] = ssegs[1][np.argmin(Dseg[reference_point[0]][ssegs[1]])] - closest_points[1, 0] = ssegs[0][np.argmin(Dseg[reference_point[1]][ssegs[0]])] - closest_points[0, 2] = ssegs[2][np.argmin(Dseg[reference_point[0]][ssegs[2]])] - closest_points[2, 0] = ssegs[0][np.argmin(Dseg[reference_point[2]][ssegs[0]])] - closest_points[1, 2] = ssegs[2][np.argmin(Dseg[reference_point[1]][ssegs[2]])] - closest_points[2, 1] = ssegs[1][np.argmin(Dseg[reference_point[2]][ssegs[1]])] + closest_points[0, 1] = ssegs[1][ + np.argmin(Dseg[reference_point[0]][ssegs[1]]) + ] + closest_points[1, 0] = ssegs[0][ + np.argmin(Dseg[reference_point[1]][ssegs[0]]) + ] + closest_points[0, 2] = ssegs[2][ + np.argmin(Dseg[reference_point[0]][ssegs[2]]) + ] + closest_points[2, 0] = ssegs[0][ + np.argmin(Dseg[reference_point[2]][ssegs[0]]) + ] + closest_points[1, 2] = ssegs[2][ + np.argmin(Dseg[reference_point[1]][ssegs[2]]) + ] + closest_points[2, 1] = ssegs[1][ + np.argmin(Dseg[reference_point[2]][ssegs[1]]) + ] added_dist = np.zeros(3) added_dist[0] = ( - Dseg[closest_points[1, 0], closest_points[0, 1]] + Dseg[closest_points[2, 0], closest_points[0, 2]] + Dseg[closest_points[1, 0], closest_points[0, 1]] + + Dseg[closest_points[2, 0], closest_points[0, 2]] ) added_dist[1] = ( - Dseg[closest_points[0, 1], closest_points[1, 0]] + Dseg[closest_points[2, 1], closest_points[1, 2]] + Dseg[closest_points[0, 1], closest_points[1, 0]] + + Dseg[closest_points[2, 1], closest_points[1, 2]] ) added_dist[2] = ( - Dseg[closest_points[1, 2], closest_points[2, 1]] + Dseg[closest_points[0, 2], closest_points[2, 0]] + Dseg[closest_points[1, 2], closest_points[2, 1]] + + Dseg[closest_points[0, 2], closest_points[2, 0]] ) trunk = np.argmin(added_dist) - ssegs_adjacency = [[trunk] if i != trunk else [j for j in range(3) if j != trunk] for i in range(3)] + ssegs_adjacency = [ + [trunk] if i != trunk else [j for j in range(3) if j != trunk] + for i in range(3) + ] ssegs_connects = [ - [closest_points[i, trunk]] if i != trunk else [closest_points[trunk, j] for j in range(3) if j != trunk] + [closest_points[i, trunk]] + if i != trunk + else [closest_points[trunk, j] for j in range(3) if j != trunk] for i in range(3) ] else: trunk = 0 ssegs_adjacency = [[1], [0]] reference_point_in_0 = ssegs_tips[0][0] - closest_point_in_1 = ssegs[1][np.argmin(Dseg[reference_point_in_0][ssegs[1]])] + closest_point_in_1 = ssegs[1][ + np.argmin(Dseg[reference_point_in_0][ssegs[1]]) + ] reference_point_in_1 = closest_point_in_1 # ssegs_tips[1][0] - closest_point_in_0 = ssegs[0][np.argmin(Dseg[reference_point_in_1][ssegs[0]])] + closest_point_in_0 = ssegs[0][ + np.argmin(Dseg[reference_point_in_1][ssegs[0]]) + ] ssegs_connects = [[closest_point_in_1], [closest_point_in_0]] return ssegs, ssegs_tips, ssegs_adjacency, ssegs_connects, trunk @@ -834,7 +936,9 @@ def _detect_branching_single_wolf17_bi(self, Dseg, tips): ssegs = [closer_to_0_than_to_1, ~closer_to_0_than_to_1] return ssegs - def __detect_branching_haghverdi16(self, Dseg: np.ndarray, tips: np.ndarray) -> np.ndarray: + def __detect_branching_haghverdi16( + self, Dseg: np.ndarray, tips: np.ndarray + ) -> np.ndarray: """\ Detect branching on given segment. @@ -872,7 +976,9 @@ def __detect_branching_haghverdi16(self, Dseg: np.ndarray, tips: np.ndarray) -> # highly different, one would need to write the following equation # in terms of an ordering, such as exploited by the kendall # correlation method above - imax = np.argmin(Dseg[tips[0]][idcs] + Dseg[tips[1]][idcs] + Dseg[tips[2]][idcs]) + imax = np.argmin( + Dseg[tips[0]][idcs] + Dseg[tips[1]][idcs] + Dseg[tips[2]][idcs] + ) # init list to store new segments ssegs = [] # noqa: F841 # first new segment: all points until, but excluding the branching point diff --git a/scanpy/tools/_draw_graph.py b/scanpy/tools/_draw_graph.py index a8c55f3fa7..0447880388 100644 --- a/scanpy/tools/_draw_graph.py +++ b/scanpy/tools/_draw_graph.py @@ -156,7 +156,9 @@ def draw_graph( iterations = kwds['iterations'] else: iterations = 500 - positions = forceatlas2.forceatlas2(adjacency, pos=init_coords, iterations=iterations) + positions = forceatlas2.forceatlas2( + adjacency, pos=init_coords, iterations=iterations + ) positions = np.array(positions) else: # igraph doesn't use numpy seed diff --git a/scanpy/tools/_embedding_density.py b/scanpy/tools/_embedding_density.py index 64c9794db6..b946837f2c 100644 --- a/scanpy/tools/_embedding_density.py +++ b/scanpy/tools/_embedding_density.py @@ -119,7 +119,8 @@ def embedding_density( if f'X_{basis}' not in adata.obsm_keys(): raise ValueError( - "Cannot find the embedded representation " f"`adata.obsm['X_{basis}']`. Compute the embedding first." + "Cannot find the embedded representation " + f"`adata.obsm['X_{basis}']`. Compute the embedding first." ) if components is None: @@ -180,7 +181,9 @@ def embedding_density( if basis != 'diffmap': components += 1 - adata.uns[f'{density_covariate}_params'] = dict(covariate=groupby, components=components.tolist()) + adata.uns[f'{density_covariate}_params'] = dict( + covariate=groupby, components=components.tolist() + ) logg.hint( f"added\n" diff --git a/scanpy/tools/_ingest.py b/scanpy/tools/_ingest.py index ed09731c48..0941ded814 100644 --- a/scanpy/tools/_ingest.py +++ b/scanpy/tools/_ingest.py @@ -113,8 +113,12 @@ def ingest( start = logg.info('running ingest') obs = [obs] if isinstance(obs, str) else obs - embedding_method = [embedding_method] if isinstance(embedding_method, str) else embedding_method - labeling_method = [labeling_method] if isinstance(labeling_method, str) else labeling_method + embedding_method = ( + [embedding_method] if isinstance(embedding_method, str) else embedding_method + ) + labeling_method = ( + [labeling_method] if isinstance(labeling_method, str) else labeling_method + ) if len(labeling_method) == 1 and len(obs or []) > 1: labeling_method = labeling_method * len(obs) @@ -247,7 +251,9 @@ def _init_dist_search(self, dist_args): make_initialized_nnd_search, ) - self._random_init, self._tree_init = make_initialisations(dist_func, dist_args) + self._random_init, self._tree_init = make_initialisations( + dist_func, dist_args + ) _initialise_search = partial( initialise_search, init_from_random=self._random_init, @@ -379,7 +385,8 @@ def __init__(self, adata, neighbors_key=None): self._init_neighbors(adata, neighbors_key) else: raise ValueError( - f'There is no neighbors data in `adata.uns["{neighbors_key}"]`.\n' 'Please run pp.neighbors.' + f'There is no neighbors data in `adata.uns["{neighbors_key}"]`.\n' + 'Please run pp.neighbors.' ) if 'X_umap' in adata.obsm: @@ -429,7 +436,10 @@ def fit(self, adata_new): new_var_names = adata_new.var_names.str.upper() if not ref_var_names.equals(new_var_names): - raise ValueError('Variables in the new adata are different ' 'from variables in the reference adata') + raise ValueError( + 'Variables in the new adata are different ' + 'from variables in the reference adata' + ) self._obs = pd.DataFrame(index=adata_new.obs.index) self._obsm = _DimDict(adata_new.n_obs, axis=0) @@ -463,9 +473,13 @@ def neighbors(self, k=None, queue_size=5, epsilon=0.1, random_state=0): else: from umap.utils import deheap_sort - init = self._initialise_search(self._rp_forest, train, test, int(k * queue_size), rng_state=rng_state) + init = self._initialise_search( + self._rp_forest, train, test, int(k * queue_size), rng_state=rng_state + ) - result = self._search(train, self._search_graph.indptr, self._search_graph.indices, init, test) + result = self._search( + train, self._search_graph.indptr, self._search_graph.indices, init, test + ) indices, dists = deheap_sort(result) self._indices, self._distances = indices[:, :k], dists[:, :k] @@ -485,10 +499,14 @@ def map_embedding(self, method): elif method == 'pca': self._obsm['X_pca'] = self._pca() else: - raise NotImplementedError('Ingest supports only umap and pca embeddings for now.') + raise NotImplementedError( + 'Ingest supports only umap and pca embeddings for now.' + ) def _knn_classify(self, labels): - cat_array = self._adata_ref.obs[labels].astype('category') # ensure it's categorical + cat_array = self._adata_ref.obs[labels].astype( + 'category' + ) # ensure it's categorical values = [cat_array[inds].mode()[0] for inds in self._indices] return pd.Categorical(values=values, categories=cat_array.cat.categories) @@ -524,7 +542,9 @@ def to_adata(self, inplace=False): if not inplace: return adata - def to_adata_joint(self, batch_key='batch', batch_categories=None, index_unique='-'): + def to_adata_joint( + self, batch_key='batch', batch_categories=None, index_unique='-' + ): """\ Returns concatenated object. @@ -545,10 +565,14 @@ def to_adata_joint(self, batch_key='batch', batch_categories=None, index_unique= for key in self._obsm: if key in self._adata_ref.obsm: - adata.obsm[key] = np.vstack((self._adata_ref.obsm[key], self._obsm[key])) + adata.obsm[key] = np.vstack( + (self._adata_ref.obsm[key], self._obsm[key]) + ) if self._use_rep not in ('X_pca', 'X'): - adata.obsm[self._use_rep] = np.vstack((self._adata_ref.obsm[self._use_rep], self._obsm['rep'])) + adata.obsm[self._use_rep] = np.vstack( + (self._adata_ref.obsm[self._use_rep], self._obsm['rep']) + ) if 'X_umap' in self._obsm: adata.uns['umap'] = self._adata_ref.uns['umap'] diff --git a/scanpy/tools/_louvain.py b/scanpy/tools/_louvain.py index d8597a7588..9b09740284 100644 --- a/scanpy/tools/_louvain.py +++ b/scanpy/tools/_louvain.py @@ -108,7 +108,9 @@ def louvain( partition_kwargs = dict(partition_kwargs) start = logg.info('running Louvain clustering') if (flavor != 'vtraag') and (partition_type is not None): - raise ValueError('`partition_type` is only a valid argument ' 'when `flavour` is "vtraag"') + raise ValueError( + '`partition_type` is only a valid argument ' 'when `flavour` is "vtraag"' + ) adata = adata.copy() if copy else adata if adjacency is None: adjacency = _choose_graph(adata, obsp, neighbors_key) @@ -180,7 +182,12 @@ def louvain( logg.info(' using the "louvain" package of rapids') louvain_parts, _ = cugraph.louvain(g) - groups = louvain_parts.to_pandas().sort_values('vertex')[['partition']].to_numpy().ravel() + groups = ( + louvain_parts.to_pandas() + .sort_values('vertex')[['partition']] + .to_numpy() + .ravel() + ) elif flavor == 'taynaud': # this is deprecated import networkx as nx diff --git a/scanpy/tools/_marker_gene_overlap.py b/scanpy/tools/_marker_gene_overlap.py index 31b1d176dd..d50932216a 100644 --- a/scanpy/tools/_marker_gene_overlap.py +++ b/scanpy/tools/_marker_gene_overlap.py @@ -21,7 +21,10 @@ def _calc_overlap_count(markers1: dict, markers2: dict): overlaps = np.zeros((len(markers1), len(markers2))) for j, marker_group in enumerate(markers1): - tmp = [len(markers2[i].intersection(markers1[marker_group])) for i in markers2.keys()] + tmp = [ + len(markers2[i].intersection(markers1[marker_group])) + for i in markers2.keys() + ] overlaps[j, :] = tmp return overlaps @@ -56,7 +59,8 @@ def _calc_jaccard(markers1: dict, markers2: dict): for j, marker_group in enumerate(markers1): tmp = [ - len(markers2[i].intersection(markers1[marker_group])) / len(markers2[i].union(markers1[marker_group])) + len(markers2[i].intersection(markers1[marker_group])) + / len(markers2[i].union(markers1[marker_group])) for i in markers2.keys() ] jacc_results[j, :] = tmp @@ -154,11 +158,15 @@ def marker_gene_overlap( # Test user inputs if inplace: raise NotImplementedError( - 'Writing Pandas dataframes to h5ad is currently under development.' '\nPlease use `inplace=False`.' + 'Writing Pandas dataframes to h5ad is currently under development.' + '\nPlease use `inplace=False`.' ) if key not in adata.uns: - raise ValueError('Could not find marker gene data. ' 'Please run `sc.tl.rank_genes_groups()` first.') + raise ValueError( + 'Could not find marker gene data. ' + 'Please run `sc.tl.rank_genes_groups()` first.' + ) avail_methods = {'overlap_count', 'overlap_coef', 'jaccard', 'enrich'} if method not in avail_methods: @@ -176,9 +184,14 @@ def marker_gene_overlap( if not all(isinstance(val, cabc.Set) for val in reference_markers.values()): try: - reference_markers = {key: set(val) for key, val in reference_markers.items()} + reference_markers = { + key: set(val) for key, val in reference_markers.items() + } except Exception: - raise ValueError('Please ensure that `reference_markers` contains ' 'sets or lists of markers as values.') + raise ValueError( + 'Please ensure that `reference_markers` contains ' + 'sets or lists of markers as values.' + ) if adj_pval_threshold is not None: if 'pvals_adj' not in adata.uns[key]: @@ -189,19 +202,26 @@ def marker_gene_overlap( ) if adj_pval_threshold < 0: - logg.warning('`adj_pval_threshold` was set below 0. Threshold will be set to 0.') + logg.warning( + '`adj_pval_threshold` was set below 0. Threshold will be set to 0.' + ) adj_pval_threshold = 0 elif adj_pval_threshold > 1: - logg.warning('`adj_pval_threshold` was set above 1. Threshold will be set to 1.') + logg.warning( + '`adj_pval_threshold` was set above 1. Threshold will be set to 1.' + ) adj_pval_threshold = 1 if top_n_markers is not None: logg.warning( - 'Both `adj_pval_threshold` and `top_n_markers` is set. ' '`adj_pval_threshold` will be ignored.' + 'Both `adj_pval_threshold` and `top_n_markers` is set. ' + '`adj_pval_threshold` will be ignored.' ) if top_n_markers is not None and top_n_markers < 1: - logg.warning('`top_n_markers` was set below 1. `top_n_markers` will be set to 1.') + logg.warning( + '`top_n_markers` was set below 1. `top_n_markers` will be set to 1.' + ) top_n_markers = 1 # Get data-derived marker genes in a dictionary of sets @@ -229,12 +249,16 @@ def marker_gene_overlap( marker_match = _calc_overlap_count(reference_markers, data_markers) if normalize == 'reference': # Ensure rows sum to 1 - ref_lengths = np.array([len(reference_markers[m_group]) for m_group in reference_markers]) + ref_lengths = np.array( + [len(reference_markers[m_group]) for m_group in reference_markers] + ) marker_match = marker_match / ref_lengths[:, np.newaxis] marker_match = np.nan_to_num(marker_match) elif normalize == 'data': # Ensure columns sum to 1 - data_lengths = np.array([len(data_markers[dat_group]) for dat_group in data_markers]) + data_lengths = np.array( + [len(data_markers[dat_group]) for dat_group in data_markers] + ) marker_match = marker_match / data_lengths marker_match = np.nan_to_num(marker_match) elif method == 'overlap_coef': @@ -252,7 +276,9 @@ def marker_gene_overlap( # Create a pandas dataframe with the results marker_groups = list(reference_markers.keys()) clusters = list(cluster_ids) - marker_matching_df = pd.DataFrame(marker_match, index=marker_groups, columns=clusters) + marker_matching_df = pd.DataFrame( + marker_match, index=marker_groups, columns=clusters + ) # Store the results if inplace: diff --git a/scanpy/tools/_paga.py b/scanpy/tools/_paga.py index 1cae9918df..d2000b23e9 100644 --- a/scanpy/tools/_paga.py +++ b/scanpy/tools/_paga.py @@ -97,7 +97,9 @@ def paga( """ check_neighbors = 'neighbors' if neighbors_key is None else neighbors_key if check_neighbors not in adata.uns: - raise ValueError('You need to run `pp.neighbors` first to compute a neighborhood graph.') + raise ValueError( + 'You need to run `pp.neighbors` first to compute a neighborhood graph.' + ) if groups is None: for k in ("leiden", "louvain"): if k in adata.obs.columns: @@ -158,7 +160,9 @@ def compute_connectivities(self): elif self._model == 'v1.0': return self._compute_connectivities_v1_0() else: - raise ValueError(f'`model` {self._model} needs to be one of {_AVAIL_MODELS}.') + raise ValueError( + f'`model` {self._model} needs to be one of {_AVAIL_MODELS}.' + ) def _compute_connectivities_v1_2(self): import igraph @@ -167,7 +171,9 @@ def _compute_connectivities_v1_2(self): ones.data = np.ones(len(ones.data)) # should be directed if we deal with distances g = _utils.get_igraph_from_adjacency(ones, directed=True) - vc = igraph.VertexClustering(g, membership=self._adata.obs[self._groups_key].cat.codes.values) + vc = igraph.VertexClustering( + g, membership=self._adata.obs[self._groups_key].cat.codes.values + ) ns = vc.sizes() n = sum(ns) es_inner_cluster = [vc.subgraph(i).ecount() for i in range(len(ns))] @@ -201,7 +207,9 @@ def _compute_connectivities_v1_0(self): ones = self._neighbors.connectivities.copy() ones.data = np.ones(len(ones.data)) g = _utils.get_igraph_from_adjacency(ones) - vc = igraph.VertexClustering(g, membership=self._adata.obs[self._groups_key].cat.codes.values) + vc = igraph.VertexClustering( + g, membership=self._adata.obs[self._groups_key].cat.codes.values + ) ns = vc.sizes() cg = vc.cluster_graph(combine_edges='sum') inter_es = _utils.get_sparse_from_igraph(cg, weight_attr='weight') / 2 @@ -226,8 +234,13 @@ def _get_connectivities_tree_v1_2(self): inverse_connectivities = self.connectivities.copy() inverse_connectivities.data = 1.0 / inverse_connectivities.data connectivities_tree = minimum_spanning_tree(inverse_connectivities) - connectivities_tree_indices = [connectivities_tree[i].nonzero()[1] for i in range(connectivities_tree.shape[0])] - connectivities_tree = sp.sparse.lil_matrix(self.connectivities.shape, dtype=float) + connectivities_tree_indices = [ + connectivities_tree[i].nonzero()[1] + for i in range(connectivities_tree.shape[0]) + ] + connectivities_tree = sp.sparse.lil_matrix( + self.connectivities.shape, dtype=float + ) for i, neighbors in enumerate(connectivities_tree_indices): if len(neighbors) > 0: connectivities_tree[i, neighbors] = self.connectivities[i, neighbors] @@ -237,7 +250,10 @@ def _get_connectivities_tree_v1_0(self, inter_es): inverse_inter_es = inter_es.copy() inverse_inter_es.data = 1.0 / inverse_inter_es.data connectivities_tree = minimum_spanning_tree(inverse_inter_es) - connectivities_tree_indices = [connectivities_tree[i].nonzero()[1] for i in range(connectivities_tree.shape[0])] + connectivities_tree_indices = [ + connectivities_tree[i].nonzero()[1] + for i in range(connectivities_tree.shape[0]) + ] connectivities_tree = sp.sparse.lil_matrix(inter_es.shape, dtype=float) for i, neighbors in enumerate(connectivities_tree_indices): if len(neighbors) > 0: @@ -249,7 +265,9 @@ def compute_transitions(self): if vkey not in self._adata.uns: if 'velocyto_transitions' in self._adata.uns: self._adata.uns[vkey] = self._adata.uns['velocyto_transitions'] - logg.debug("The key 'velocyto_transitions' has been changed to 'velocity_graph'.") + logg.debug( + "The key 'velocyto_transitions' has been changed to 'velocity_graph'." + ) else: raise ValueError( 'The passed AnnData needs to have an `uns` annotation ' @@ -270,7 +288,9 @@ def compute_transitions(self): self._adata.uns[vkey].astype('bool'), directed=True, ) - vc = igraph.VertexClustering(g, membership=self._adata.obs[self._groups_key].cat.codes.values) + vc = igraph.VertexClustering( + g, membership=self._adata.obs[self._groups_key].cat.codes.values + ) # set combine_edges to False if you want self loops cg_full = vc.cluster_graph(combine_edges='sum') transitions = _utils.get_sparse_from_igraph(cg_full, weight_attr='weight') @@ -301,7 +321,9 @@ def compute_transitions_old(self): self._adata.uns['velocyto_transitions'], directed=True, ) - vc = igraph.VertexClustering(g, membership=self._adata.obs[self._groups_key].cat.codes.values) + vc = igraph.VertexClustering( + g, membership=self._adata.obs[self._groups_key].cat.codes.values + ) # this stores all single-cell edges in the cluster graph cg_full = vc.cluster_graph(combine_edges=False) # this is the boolean version that simply counts edges in the clustered graph @@ -309,7 +331,9 @@ def compute_transitions_old(self): self._adata.uns['velocyto_transitions'].astype('bool'), directed=True, ) - vc_bool = igraph.VertexClustering(g_bool, membership=self._adata.obs[self._groups_key].cat.codes.values) + vc_bool = igraph.VertexClustering( + g_bool, membership=self._adata.obs[self._groups_key].cat.codes.values + ) cg_bool = vc_bool.cluster_graph(combine_edges='sum') # collapsed version transitions = _utils.get_sparse_from_igraph(cg_bool, weight_attr='weight') total_n = self._neighbors.n_neighbors * np.array(vc_bool.sizes()) @@ -390,12 +414,16 @@ def paga_expression_entropies(adata) -> List[float]: """ from scipy.stats import entropy - groups_order, groups_masks = _utils.select_groups(adata, key=adata.uns['paga']['groups']) + groups_order, groups_masks = _utils.select_groups( + adata, key=adata.uns['paga']['groups'] + ) entropies = [] for mask in groups_masks: X_mask = adata.X[mask].todense() x_median = np.nanmedian(X_mask, axis=1, overwrite_input=True) - x_probs = (x_median - np.nanmin(x_median)) / (np.nanmax(x_median) - np.nanmin(x_median)) + x_probs = (x_median - np.nanmin(x_median)) / ( + np.nanmax(x_median) - np.nanmin(x_median) + ) entropies.append(entropy(x_probs)) return entropies @@ -449,7 +477,11 @@ def paga_compare_paths( import networkx as nx g1 = nx.Graph(adata1.uns['paga'][adjacency_key]) - g2 = nx.Graph(adata2.uns['paga'][adjacency_key2 if adjacency_key2 is not None else adjacency_key]) + g2 = nx.Graph( + adata2.uns['paga'][ + adjacency_key2 if adjacency_key2 is not None else adjacency_key + ] + ) leaf_nodes1 = [str(x) for x in g1.nodes() if g1.degree(x) == 1] logg.debug(f'leaf nodes in graph 1: {leaf_nodes1}') paga_groups = adata1.uns['paga']['groups'] @@ -527,7 +559,10 @@ def paga_compare_paths( if ( ip < ip_progress or l not in p - or not (ip + 1 < len(path_mapped) and path_compare[il + 1] in path_mapped[ip + 1]) + or not ( + ip + 1 < len(path_mapped) + and path_compare[il + 1] in path_mapped[ip + 1] + ) ): continue # make sure that a step backward leads us to the same value of l @@ -545,7 +580,8 @@ def paga_compare_paths( # was ok in the previous step poss = list(range(ip - 1, ip_progress - 2, -1)) logg.debug( - f' step(s) backward to position(s) {poss} ' 'in path_mapped are fine, too: valid step' + f' step(s) backward to position(s) {poss} ' + 'in path_mapped are fine, too: valid step' ) n_agreeing_steps_path += 1 ip_progress = ip + 1 diff --git a/scanpy/tools/_rank_genes_groups.py b/scanpy/tools/_rank_genes_groups.py index e096f5fd34..99393f691c 100644 --- a/scanpy/tools/_rank_genes_groups.py +++ b/scanpy/tools/_rank_genes_groups.py @@ -104,7 +104,9 @@ def __init__( else: self.expm1_func = np.expm1 - self.groups_order, self.groups_masks = _utils.select_groups(adata, groups, groupby) + self.groups_order, self.groups_masks = _utils.select_groups( + adata, groups, groupby + ) # Singlet groups cause division by zero errors invalid_groups_selected = set(self.groups_order) & set( @@ -169,7 +171,9 @@ def _basic_stats(self): else: mask_rest = self.groups_masks[self.ireference] X_rest = self.X[mask_rest] - self.means[self.ireference], self.vars[self.ireference] = _get_mean_var(X_rest) + self.means[self.ireference], self.vars[self.ireference] = _get_mean_var( + X_rest + ) # deleting the next line causes a memory leak for some reason del X_rest @@ -280,7 +284,10 @@ def wilcoxon(self, tie_correct): m_active = np.count_nonzero(mask_rest) if n_active <= 25 or m_active <= 25: - logg.hint('Few observations in a group for ' 'normal approximation (<=25). Lower test accuracy.') + logg.hint( + 'Few observations in a group for ' + 'normal approximation (<=25). Lower test accuracy.' + ) # Calculate rank sums for each chunk for the current mask for ranks, left, right in _ranks(self.X, mask, mask_rest): @@ -288,9 +295,13 @@ def wilcoxon(self, tie_correct): if tie_correct: T[left:right] = _tiecorrect(ranks) - std_dev = np.sqrt(T * n_active * m_active * (n_active + m_active + 1) / 12.0) + std_dev = np.sqrt( + T * n_active * m_active * (n_active + m_active + 1) / 12.0 + ) - scores = (scores - (n_active * ((n_active + m_active + 1) / 2.0))) / std_dev + scores = ( + scores - (n_active * ((n_active + m_active + 1) / 2.0)) + ) / std_dev scores[np.isnan(scores)] = 0 pvals = 2 * stats.distributions.norm.sf(np.abs(scores)) @@ -320,9 +331,13 @@ def wilcoxon(self, tie_correct): else: T_i = 1 - std_dev = np.sqrt(T_i * n_active * (n_cells - n_active) * (n_cells + 1) / 12.0) + std_dev = np.sqrt( + T_i * n_active * (n_cells - n_active) * (n_cells + 1) / 12.0 + ) - scores[group_index, :] = (scores[group_index, :] - (n_active * (n_cells + 1) / 2.0)) / std_dev + scores[group_index, :] = ( + scores[group_index, :] - (n_active * (n_cells + 1) / 2.0) + ) / std_dev scores[np.isnan(scores)] = 0 pvals = 2 * stats.distributions.norm.sf(np.abs(scores[group_index, :])) @@ -400,7 +415,9 @@ def compute_statistics( from statsmodels.stats.multitest import multipletests pvals[np.isnan(pvals)] = 1 - _, pvals_adj, _, _ = multipletests(pvals, alpha=0.05, method='fdr_bh') + _, pvals_adj, _, _ = multipletests( + pvals, alpha=0.05, method='fdr_bh' + ) elif corr_method == 'bonferroni': pvals_adj = np.minimum(pvals * n_genes, 1.0) self.stats[group_name, 'pvals_adj'] = pvals_adj[global_indices] @@ -414,7 +431,9 @@ def compute_statistics( foldchanges = (self.expm1_func(mean_group) + 1e-9) / ( self.expm1_func(mean_rest) + 1e-9 ) # add small value to remove 0's - self.stats[group_name, 'logfoldchanges'] = np.log2(foldchanges[global_indices]) + self.stats[group_name, 'logfoldchanges'] = np.log2( + foldchanges[global_indices] + ) if n_genes_user is None: self.stats.index = self.var_names @@ -529,7 +548,9 @@ def rank_genes_groups( >>> sc.pl.rank_genes_groups(adata) """ if method is None: - logg.warning("Default of the method has been changed to 't-test' from 't-test_overestim_var'") + logg.warning( + "Default of the method has been changed to 't-test' from 't-test_overestim_var'" + ) method = 't-test' if 'only_positive' in kwds: @@ -559,7 +580,9 @@ def rank_genes_groups( groups_order += [reference] if reference != 'rest' and reference not in adata.obs[groupby].cat.categories: cats = adata.obs[groupby].cat.categories.tolist() - raise ValueError(f'reference = {reference} needs to be one of groupby = {cats}.') + raise ValueError( + f'reference = {reference} needs to be one of groupby = {cats}.' + ) if key_added is None: key_added = 'rank_genes_groups' @@ -585,11 +608,15 @@ def rank_genes_groups( logg.debug(f'consider {groupby!r} groups:') logg.debug(f'with sizes: {np.count_nonzero(test_obj.groups_masks, axis=1)}') - test_obj.compute_statistics(method, corr_method, n_genes_user, rankby_abs, tie_correct, **kwds) + test_obj.compute_statistics( + method, corr_method, n_genes_user, rankby_abs, tie_correct, **kwds + ) if test_obj.pts is not None: groups_names = [str(name) for name in test_obj.groups_order] - adata.uns[key_added]['pts'] = pd.DataFrame(test_obj.pts.T, index=test_obj.var_names, columns=groups_names) + adata.uns[key_added]['pts'] = pd.DataFrame( + test_obj.pts.T, index=test_obj.var_names, columns=groups_names + ) if test_obj.pts_rest is not None: adata.uns[key_added]['pts_rest'] = pd.DataFrame( test_obj.pts_rest.T, index=test_obj.var_names, columns=groups_names @@ -606,7 +633,9 @@ def rank_genes_groups( } for col in test_obj.stats.columns.levels[0]: - adata.uns[key_added][col] = test_obj.stats[col].to_records(index=False, column_dtypes=dtypes[col]) + adata.uns[key_added][col] = test_obj.stats[col].to_records( + index=False, column_dtypes=dtypes[col] + ) logg.info( ' finished', @@ -751,8 +780,12 @@ def expm1_func(x): X_out = sub_X[~in_group] if use_fraction: - fraction_in_cluster_matrix.loc[:, cluster] = adata.uns[key]['pts'][cluster].loc[var_names].values - fraction_out_cluster_matrix.loc[:, cluster] = adata.uns[key]['pts_rest'][cluster].loc[var_names].values + fraction_in_cluster_matrix.loc[:, cluster] = ( + adata.uns[key]['pts'][cluster].loc[var_names].values + ) + fraction_out_cluster_matrix.loc[:, cluster] = ( + adata.uns[key]['pts_rest'][cluster].loc[var_names].values + ) else: fraction_in_cluster_matrix.loc[:, cluster] = _calc_frac(X_in) fraction_out_cluster_matrix.loc[:, cluster] = _calc_frac(X_out) @@ -763,7 +796,8 @@ def expm1_func(x): mean_out_cluster = np.ravel(X_out.mean(0)) # compute fold change fold_change_matrix.loc[:, cluster] = np.log2( - (expm1_func(mean_in_cluster) + 1e-9) / (expm1_func(mean_out_cluster) + 1e-9) + (expm1_func(mean_in_cluster) + 1e-9) + / (expm1_func(mean_out_cluster) + 1e-9) ) # filter original_matrix diff --git a/scanpy/tools/_score_genes.py b/scanpy/tools/_score_genes.py index a02dfafa12..d336544f0d 100644 --- a/scanpy/tools/_score_genes.py +++ b/scanpy/tools/_score_genes.py @@ -32,7 +32,9 @@ def _sparse_nanmean(X, axis): # the average s = Y.sum(axis) - m = s / n_elements.astype('float32') # if we dont cast the int32 to float32, this will result in float64... + m = s / n_elements.astype( + 'float32' + ) # if we dont cast the int32 to float32, this will result in float64... return m @@ -125,16 +127,22 @@ def score_genes( use_raw = _check_use_raw(adata, use_raw) _adata = adata.raw if use_raw else adata - _adata_subset = _adata[:, gene_pool] if len(gene_pool) < len(_adata.var_names) else _adata + _adata_subset = ( + _adata[:, gene_pool] if len(gene_pool) < len(_adata.var_names) else _adata + ) if issparse(_adata_subset.X): obs_avg = pd.Series( np.array(_sparse_nanmean(_adata_subset.X, axis=0)).flatten(), index=gene_pool, ) # average expression of genes else: - obs_avg = pd.Series(np.nanmean(_adata_subset.X, axis=0), index=gene_pool) # average expression of genes + obs_avg = pd.Series( + np.nanmean(_adata_subset.X, axis=0), index=gene_pool + ) # average expression of genes - obs_avg = obs_avg[np.isfinite(obs_avg)] # Sometimes (and I don't know how) missing data may be there, with nansfor + obs_avg = obs_avg[ + np.isfinite(obs_avg) + ] # Sometimes (and I don't know how) missing data may be there, with nansfor n_items = int(np.round(len(obs_avg) / (n_bins - 1))) obs_cut = obs_avg.rank(method='min') // n_items @@ -231,7 +239,9 @@ def score_genes_cell_cycle( adata = adata.copy() if copy else adata ctrl_size = min(len(s_genes), len(g2m_genes)) # add s-score - score_genes(adata, gene_list=s_genes, score_name='S_score', ctrl_size=ctrl_size, **kwargs) + score_genes( + adata, gene_list=s_genes, score_name='S_score', ctrl_size=ctrl_size, **kwargs + ) # add g2m-score score_genes( adata, diff --git a/scanpy/tools/_sim.py b/scanpy/tools/_sim.py index e2e4fcbb68..5bf79f8189 100644 --- a/scanpy/tools/_sim.py +++ b/scanpy/tools/_sim.py @@ -164,7 +164,9 @@ def sample_dynamic_data(**params): for restart in range(nrRealizations + maxRestarts): # slightly break symmetry in initial conditions if 'toggleswitch' in model_key: - X0 = np.array([0.8 for i in range(grnsim.dim)]) + 0.01 * np.random.randn(grnsim.dim) + X0 = np.array( + [0.8 for i in range(grnsim.dim)] + ) + 0.01 * np.random.randn(grnsim.dim) X = grnsim.sim_model(tmax=tmax, X0=X0, noiseDyn=noiseDyn) # check branching check = True @@ -259,7 +261,9 @@ def sample_dynamic_data(**params): for filename in writedir.glob('sim*.txt'): pass logg.info(f'reading simulation results {filename}') - adata = readwrite._read(filename, first_column_names=True, suppress_cache_warning=True) + adata = readwrite._read( + filename, first_column_names=True, suppress_cache_warning=True + ) adata.uns['tmax_write'] = tmax / step return adata @@ -307,12 +311,16 @@ def write_data( Adj[i, i] = 1 np.savetxt(dir + '/adj_' + id + '.txt', Adj, header=header, fmt='%d') if Coupl.size > 0: - np.savetxt(dir + '/coupl_' + id + '.txt', Coupl, header=header, fmt='%10.6f') + np.savetxt( + dir + '/coupl_' + id + '.txt', Coupl, header=header, fmt='%10.6f' + ) # write model file if varNames and Coupl.size > 0: with (dir / f'model_{id}.txt').open('w') as f: f.write('# For each "variable = ", there must be a right hand side: \n') - f.write('# either an empty string or a python-style logical expression \n') + f.write( + '# either an empty string or a python-style logical expression \n' + ) f.write('# involving variable names, "or", "and", "(", ")". \n') f.write('# The order of equations matters! \n') f.write('# \n') @@ -328,7 +336,11 @@ def write_data( for gp in range(dim): for g in range(dim): if np.abs(Coupl[gp, g]) > 1e-10: - f.write(f'{names[gp]:10} ' f'{names[g]:10} ' f'{Coupl[gp, g]:10.3} \n') + f.write( + f'{names[gp]:10} ' + f'{names[g]:10} ' + f'{Coupl[gp, g]:10.3} \n' + ) # write simulated data # the binary mode option in the following line is a fix for python 3 # variable names @@ -384,7 +396,9 @@ def __init__( either string for predefined model, or directory with a model file and a couple matrix files """ - self.dim = dim if Coupl is None else Coupl.shape[0] # number of nodes / dimension of system + self.dim = ( + dim if Coupl is None else Coupl.shape[0] + ) # number of nodes / dimension of system self.maxnpar = 1 # maximal number of parents self.p_indep = 0.4 # fraction of independent genes self.model = model @@ -461,16 +475,24 @@ def Xdiff_hill(self, Xt): iparent = self.varNames[self.pas[child][iv]] x = Xt[iparent] threshold = 0.1 / np.abs(self.Coupl[ichild, iparent]) - Xdiff_syn_tuple *= self.hill_a(x, threshold) if v else self.hill_i(x, threshold) + Xdiff_syn_tuple *= ( + self.hill_a(x, threshold) if v else self.hill_i(x, threshold) + ) if verbosity > 0: - Xdiff_syn_tuple_str += f'{"a" if v else "i"}' f'({self.pas[child][iv]}, {threshold:.2})' + Xdiff_syn_tuple_str += ( + f'{"a" if v else "i"}' + f'({self.pas[child][iv]}, {threshold:.2})' + ) Xdiff_syn += Xdiff_syn_tuple if verbosity > 0: Xdiff_syn_str += ('+' if ituple != 0 else '') + Xdiff_syn_tuple_str # multiply with degradation term Xdiff[ichild] = self.invTimeStep * (Xdiff_syn - Xt[ichild]) if verbosity > 0: - Xdiff_str = f'{child}_{child}-{child} = ' f'{self.invTimeStep}*({Xdiff_syn_str}-{child})' + Xdiff_str = ( + f'{child}_{child}-{child} = ' + f'{self.invTimeStep}*({Xdiff_syn_str}-{child})' + ) settings.m(0, Xdiff_str) return Xdiff @@ -540,7 +562,9 @@ def read_model(self): # read couplings via names self.Coupl = np.zeros((self.dim, self.dim)) boolContinue = True - for line in self.model.open(): # open(self.model.replace('/model','/couplList')): + for ( + line + ) in self.model.open(): # open(self.model.replace('/model','/couplList')): if line.startswith('# coupling list:'): boolContinue = False if boolContinue: @@ -572,7 +596,9 @@ def set_coupl(self, Coupl=None): for g in range(self.dim): if np.abs(self.Coupl[gp, g] > 1e-10): pas.append(names[g]) - self.boolRules[names[gp]] = ''.join(pas[:1] + [' or ' + pa for pa in pas[1:]]) + self.boolRules[names[gp]] = ''.join( + pas[:1] + [' or ' + pa for pa in pas[1:]] + ) self.Adj_signed = np.sign(Coupl) elif self.model in ['6', '7', '8', '9', '10']: self.Adj_signed = np.zeros((self.dim, self.dim)) @@ -591,10 +617,14 @@ def set_coupl(self, Coupl=None): # settings.m(0,leafnodes,availnodes) while len(availnodes) != 0: # parent - parent_idx = np.random.choice(np.arange(0, len(leafnodes)), size=1, replace=False) + parent_idx = np.random.choice( + np.arange(0, len(leafnodes)), size=1, replace=False + ) parent = leafnodes[parent_idx] # children - children_ids = np.random.choice(np.arange(0, len(availnodes)), size=2, replace=False) + children_ids = np.random.choice( + np.arange(0, len(availnodes)), size=2, replace=False + ) children = availnodes[children_ids] settings.m(0, parent, children) self.Adj_signed[children, parent] = np.ones(2) @@ -621,7 +651,9 @@ def set_coupl(self, Coupl=None): # and the variable itself, therefore its # self.maxnpar+2 in the following line nr = np.random.randint(1, self.maxnpar + 2) - j_par = np.random.choice(np.arange(0, self.dim), size=nr, replace=False) + j_par = np.random.choice( + np.arange(0, self.dim), size=nr, replace=False + ) self.Adj[i, j_par] = 1 else: self.Adj[i, i] = 1 @@ -706,7 +738,9 @@ def sim_model_backwards(self, tmax, X0): X = np.zeros((tmax, self.dim)) X[tmax - 1] = X0 for t in range(tmax - 2, -1, -1): - sol = sp.optimize.root(self.sim_model_back_help, X[t + 1], args=(X[t + 1]), method='hybr') + sol = sp.optimize.root( + self.sim_model_back_help, X[t + 1], args=(X[t + 1]), method='hybr' + ) X[t] = sol.x return X @@ -716,12 +750,17 @@ def branch_init_model1(self, tmax=100): if Xfix[0] > 0.97 or Xfix[0] < 0.03: settings.m( 0, - '... either no fixed point in [0,1]^2! \n' + ' or fixed point is too close to bounds', + '... either no fixed point in [0,1]^2! \n' + + ' or fixed point is too close to bounds', ) return None # - XbackUp = self.sim_model_backwards(tmax=tmax / 3, X0=Xfix + np.array([0.02, -0.02])) - XbackDo = self.sim_model_backwards(tmax=tmax / 3, X0=Xfix + np.array([-0.02, -0.02])) + XbackUp = self.sim_model_backwards( + tmax=tmax / 3, X0=Xfix + np.array([0.02, -0.02]) + ) + XbackDo = self.sim_model_backwards( + tmax=tmax / 3, X0=Xfix + np.array([-0.02, -0.02]) + ) # Xup = self.sim_model(tmax=tmax, X0=XbackUp[0]) Xdo = self.sim_model(tmax=tmax, X0=XbackDo[0]) @@ -744,7 +783,13 @@ def parents_from_boolRule(self, rule): Returns list of parents. """ - rule_pa = rule.replace('(', '').replace(')', '').replace('or', '').replace('and', '').replace('not', '') + rule_pa = ( + rule.replace('(', '') + .replace(')', '') + .replace('or', '') + .replace('and', '') + .replace('not', '') + ) rule_pa = rule_pa.split() # if there are no parents, continue if not rule_pa: @@ -791,13 +836,17 @@ def build_boolCoeff(self): raise ValueError(f'specify coupling value for {key} <- {g}') else: if np.abs(self.Coupl[self.varNames[key], g]) > 1e-10: - raise ValueError('there should be no coupling value for ' f'{key} <- {g}') + raise ValueError( + 'there should be no coupling value for ' f'{key} <- {g}' + ) if self.verbosity > 1: settings.m(0, '...' + key) settings.m(0, rule) settings.m(0, rule_pa) # noqa: F821 # now evaluate coefficients - for tuple in list(itertools.product([False, True], repeat=len(self.pas[key]))): + for tuple in list( + itertools.product([False, True], repeat=len(self.pas[key])) + ): if self.process_rule(rule, self.pas[key], tuple): self.boolCoeff[key].append(tuple) # @@ -925,7 +974,9 @@ def check_nocycles(Adj: np.ndarray, verbosity: int = 2) -> bool: return True -def sample_coupling_matrix(dim: int = 3, connectivity: float = 0.5) -> Tuple[np.ndarray, np.ndarray, np.ndarray, int]: +def sample_coupling_matrix( + dim: int = 3, connectivity: float = 0.5 +) -> Tuple[np.ndarray, np.ndarray, np.ndarray, int]: """\ Sample coupling matrix. @@ -975,7 +1026,9 @@ def sample_coupling_matrix(dim: int = 3, connectivity: float = 0.5) -> Tuple[np. check = True break if not check: - raise ValueError('did not find graph without cycles after' f'{max_trial} trials') + raise ValueError( + 'did not find graph without cycles after' f'{max_trial} trials' + ) return Coupl, Adj, Adj_signed, n_edges @@ -1167,7 +1220,8 @@ def sample_static_data(model, dir, verbosity=0): # command line options p = argparse.ArgumentParser( description=( - 'Simulate stochastic discrete-time dynamical systems,\n' 'in particular gene regulatory networks.' + 'Simulate stochastic discrete-time dynamical systems,\n' + 'in particular gene regulatory networks.' ), formatter_class=argparse.RawDescriptionHelpFormatter, epilog=( @@ -1206,7 +1260,10 @@ def sample_static_data(model, dir, verbosity=0): model = dir.name.split('_')[0] settings.m(0, f'...model is: {model!r}') if dir.is_dir() and 'test' not in str(dir): - message = f'directory {dir} already exists, ' 'remove it and continue? [y/n, press enter]' + message = ( + f'directory {dir} already exists, ' + 'remove it and continue? [y/n, press enter]' + ) if str(input(message)) != 'y': settings.m(0, ' ...quit program execution') sys.exit() diff --git a/scanpy/tools/_top_genes.py b/scanpy/tools/_top_genes.py index 855fef2222..28a9390590 100644 --- a/scanpy/tools/_top_genes.py +++ b/scanpy/tools/_top_genes.py @@ -180,7 +180,9 @@ def ROC_AUC_analysis( fpr[name_list[i]], tpr[name_list[i]], thresholds[name_list[i]], - ) = metrics.roc_curve(y_true, y_score, pos_label=None, sample_weight=None, drop_intermediate=False) + ) = metrics.roc_curve( + y_true, y_score, pos_label=None, sample_weight=None, drop_intermediate=False + ) roc_auc[name_list[i]] = metrics.auc(fpr[name_list[i]], tpr[name_list[i]]) adata.uns['ROCfpr' + groupby + str(group)] = fpr adata.uns['ROCtpr' + groupby + str(group)] = tpr diff --git a/scanpy/tools/_tsne_fix.py b/scanpy/tools/_tsne_fix.py index 8ee49cd98c..d5a7b663b5 100644 --- a/scanpy/tools/_tsne_fix.py +++ b/scanpy/tools/_tsne_fix.py @@ -125,12 +125,16 @@ def _gradient_descent( if verbose >= 2: print( "[t-SNE] Iteration %d: did not make any progress " - "during the last %d episodes. Finished." % (i + 1, n_iter_without_progress) + "during the last %d episodes. Finished." + % (i + 1, n_iter_without_progress) ) break if grad_norm <= min_grad_norm: if verbose >= 2: - print("[t-SNE] Iteration %d: gradient norm %f. Finished." % (i + 1, grad_norm)) + print( + "[t-SNE] Iteration %d: gradient norm %f. Finished." + % (i + 1, grad_norm) + ) break if error_diff <= min_error_diff: if verbose >= 2: diff --git a/scanpy/tools/_umap.py b/scanpy/tools/_umap.py index 4267fc479e..28ee17c229 100644 --- a/scanpy/tools/_umap.py +++ b/scanpy/tools/_umap.py @@ -122,13 +122,17 @@ def umap( neighbors_key = 'neighbors' if neighbors_key not in adata.uns: - raise ValueError(f'Did not find .uns["{neighbors_key}"]. Run `sc.pp.neighbors` first.') + raise ValueError( + f'Did not find .uns["{neighbors_key}"]. Run `sc.pp.neighbors` first.' + ) start = logg.info('computing UMAP') neighbors = NeighborsView(adata, neighbors_key) if 'params' not in neighbors or neighbors['params']['method'] != 'umap': - logg.warning(f'.obsp["{neighbors["connectivities_key"]}"] have not been computed using umap') + logg.warning( + f'.obsp["{neighbors["connectivities_key"]}"] have not been computed using umap' + ) # Compat for umap 0.4 -> 0.5 with warnings.catch_warnings(): @@ -163,7 +167,9 @@ def simplicial_set_embedding(*args, **kwargs): if isinstance(init_pos, str) and init_pos in adata.obsm.keys(): init_coords = adata.obsm[init_pos] elif isinstance(init_pos, str) and init_pos == 'paga': - init_coords = get_init_pos_from_paga(adata, random_state=random_state, neighbors_key=neighbors_key) + init_coords = get_init_pos_from_paga( + adata, random_state=random_state, neighbors_key=neighbors_key + ) else: init_coords = init_pos # Let umap handle it if hasattr(init_coords, "dtype"): @@ -210,7 +216,9 @@ def simplicial_set_embedding(*args, **kwargs): from cuml import UMAP n_neighbors = neighbors['params']['n_neighbors'] - n_epochs = 500 if maxiter is None else maxiter # 0 is not a valid value for rapids, unlike original umap + n_epochs = ( + 500 if maxiter is None else maxiter + ) # 0 is not a valid value for rapids, unlike original umap X_contiguous = np.ascontiguousarray(X, dtype=np.float32) umap = UMAP( n_neighbors=n_neighbors, diff --git a/scanpy/tools/_utils.py b/scanpy/tools/_utils.py index 3d257fe1d2..856a3f1b45 100644 --- a/scanpy/tools/_utils.py +++ b/scanpy/tools/_utils.py @@ -30,7 +30,9 @@ def _choose_representation(adata, use_rep=None, n_pcs=None, silent=False): if adata.n_vars > settings.N_PCS: if 'X_pca' in adata.obsm.keys(): if n_pcs is not None and n_pcs > adata.obsm['X_pca'].shape[1]: - raise ValueError('`X_pca` does not have enough PCs. Rerun `sc.pp.pca` with adjusted `n_comps`.') + raise ValueError( + '`X_pca` does not have enough PCs. Rerun `sc.pp.pca` with adjusted `n_comps`.' + ) X = adata.obsm['X_pca'][:, :n_pcs] logg.info(f' using \'X_pca\' with n_pcs = {X.shape[1]}') else: @@ -52,7 +54,10 @@ def _choose_representation(adata, use_rep=None, n_pcs=None, silent=False): elif use_rep == 'X': X = adata.X else: - raise ValueError('Did not find {} in `.obsm.keys()`. ' 'You need to compute it first.'.format(use_rep)) + raise ValueError( + 'Did not find {} in `.obsm.keys()`. ' + 'You need to compute it first.'.format(use_rep) + ) settings.verbosity = verbosity # resetting verbosity return X @@ -88,7 +93,9 @@ def preprocess_with_pca(adata, n_pcs: Optional[int] = None, random_state=0): return adata.X -def get_init_pos_from_paga(adata, adjacency=None, random_state=0, neighbors_key=None, obsp=None): +def get_init_pos_from_paga( + adata, adjacency=None, random_state=0, neighbors_key=None, obsp=None +): np.random.seed(random_state) if adjacency is None: adjacency = _choose_graph(adata, obsp, neighbors_key) @@ -110,5 +117,7 @@ def get_init_pos_from_paga(adata, adjacency=None, random_state=0, neighbors_key= else: init_pos[subset] = group_pos else: - raise ValueError('Plot PAGA first, so that adata.uns[\'paga\']' 'with key \'pos\'.') + raise ValueError( + 'Plot PAGA first, so that adata.uns[\'paga\']' 'with key \'pos\'.' + ) return init_pos diff --git a/scanpy/tools/_utils_clustering.py b/scanpy/tools/_utils_clustering.py index b46b9a2387..f7b331e6ca 100644 --- a/scanpy/tools/_utils_clustering.py +++ b/scanpy/tools/_utils_clustering.py @@ -1,4 +1,6 @@ -def rename_groups(adata, key_added, restrict_key, restrict_categories, restrict_indices, groups): +def rename_groups( + adata, key_added, restrict_key, restrict_categories, restrict_indices, groups +): key_added = restrict_key + '_R' if key_added is None else key_added all_groups = adata.obs[restrict_key].astype('U') prefix = '-'.join(restrict_categories) + ',' @@ -9,10 +11,14 @@ def rename_groups(adata, key_added, restrict_key, restrict_categories, restrict_ def restrict_adjacency(adata, restrict_key, restrict_categories, adjacency): if not isinstance(restrict_categories[0], str): - raise ValueError('You need to use strings to label categories, ' 'e.g. \'1\' instead of 1.') + raise ValueError( + 'You need to use strings to label categories, ' 'e.g. \'1\' instead of 1.' + ) for c in restrict_categories: if c not in adata.obs[restrict_key].cat.categories: - raise ValueError('\'{}\' is not a valid category for \'{}\''.format(c, restrict_key)) + raise ValueError( + '\'{}\' is not a valid category for \'{}\''.format(c, restrict_key) + ) restrict_indices = adata.obs[restrict_key].isin(restrict_categories).values adjacency = adjacency[restrict_indices, :] adjacency = adjacency[:, restrict_indices] From 893a034ba66ba4d0532599d7be278103d020b906 Mon Sep 17 00:00:00 2001 From: Zethson Date: Thu, 25 Feb 2021 12:44:50 +0100 Subject: [PATCH 42/85] black Signed-off-by: Zethson --- pyproject.toml | 2 +- scanpy/preprocessing/_deprecated/highly_variable_genes.py | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6b805040d8..436e934584 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -129,7 +129,7 @@ omit = ['*/tests/*'] [tool.black] line-length = 88 -target-version = ['py38'] +target-version = ['py36'] skip-string-normalization = true exclude = ''' /build/.* diff --git a/scanpy/preprocessing/_deprecated/highly_variable_genes.py b/scanpy/preprocessing/_deprecated/highly_variable_genes.py index 1487b60feb..570a5f9f25 100644 --- a/scanpy/preprocessing/_deprecated/highly_variable_genes.py +++ b/scanpy/preprocessing/_deprecated/highly_variable_genes.py @@ -170,10 +170,8 @@ def filter_genes_dispersion( disp_mean_bin[one_gene_per_bin] = 0 # actually do the normalization df['dispersion_norm'] = ( - df['dispersion'].values - - disp_mean_bin[ - df['mean_bin'].values - ].values # use values here as index differs + df['dispersion'].values # use values here as index differs + - disp_mean_bin[df['mean_bin'].values].values ) / disp_std_bin[df['mean_bin'].values].values elif flavor == 'cell_ranger': from statsmodels import robust From 53948bd14c494ec969bff3ac8d7c8b6e9765a6ee Mon Sep 17 00:00:00 2001 From: Zethson Date: Thu, 25 Feb 2021 14:22:16 +0100 Subject: [PATCH 43/85] using self for obs_tidy Signed-off-by: Zethson --- scanpy/plotting/_baseplot_class.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scanpy/plotting/_baseplot_class.py b/scanpy/plotting/_baseplot_class.py index 5c98ca2694..7eb1cca806 100644 --- a/scanpy/plotting/_baseplot_class.py +++ b/scanpy/plotting/_baseplot_class.py @@ -127,7 +127,7 @@ def __init__( if categories_order is not None: if set(self.obs_tidy.index.categories) != set(categories_order): logg.error( - "Please check that the categories given by " # noqa: F821 + "Please check that the categories given by " "the `order` parameter match the categories that " "want to be reordered.\n\n" "Mismatch: " From 95958ff6abba236f32cb21006d9e71a9168d0d7c Mon Sep 17 00:00:00 2001 From: Zethson Date: Mon, 15 Mar 2021 11:56:02 +0100 Subject: [PATCH 44/85] rebased Signed-off-by: Zethson --- scanpy/plotting/_baseplot_class.py | 8 -------- scanpy/preprocessing/_highly_variable_genes.py | 5 ----- 2 files changed, 13 deletions(-) diff --git a/scanpy/plotting/_baseplot_class.py b/scanpy/plotting/_baseplot_class.py index 7eb1cca806..e28a535fed 100644 --- a/scanpy/plotting/_baseplot_class.py +++ b/scanpy/plotting/_baseplot_class.py @@ -570,20 +570,12 @@ def _mainplot(self, ax): ax.set_ylim(len(y_labels), 0) ax.set_xlim(0, len(x_labels)) -<<<<<<< HEAD return check_colornorm( self.vboundnorm.vmin, self.vboundnorm.vmax, self.vboundnorm.vcenter, self.vboundnorm.norm, ) -======= - normalize = matplotlib.colors.Normalize( - vmin=self.kwds.get('vmin'), vmax=self.kwds.get('vmax') - ) - - return normalize ->>>>>>> 617168f7 (address review) def make_figure(self): """ diff --git a/scanpy/preprocessing/_highly_variable_genes.py b/scanpy/preprocessing/_highly_variable_genes.py index 9eb224fe4d..039a57cb58 100644 --- a/scanpy/preprocessing/_highly_variable_genes.py +++ b/scanpy/preprocessing/_highly_variable_genes.py @@ -57,16 +57,11 @@ def _highly_variable_genes_seurat_v3( ) X = adata.layers[layer] if layer is not None else adata.X -<<<<<<< HEAD if check_values and (check_nonnegative_integers(X) == False): warnings.warn( "`flavor='seurat_v3'` expects raw count data, but non-integers were found.", UserWarning, ) -======= - if check_nonnegative_integers(X) is False: - raise ValueError("`pp.highly_variable_genes` with `flavor='seurat_v3'` expects " "raw count data.") ->>>>>>> 40dc2c3b (add flake8 pre-commit) if batch_key is None: batch_info = pd.Categorical(np.zeros(adata.shape[0], dtype=int)) From 99e12187bfc4ff5070e247c750237333462851a6 Mon Sep 17 00:00:00 2001 From: Zethson Date: Mon, 15 Mar 2021 11:59:39 +0100 Subject: [PATCH 45/85] rebasing Signed-off-by: Zethson --- scanpy/plotting/_dotplot.py | 9 --------- scanpy/tests/external/test_scrublet.py | 2 -- scanpy/tests/helpers.py | 1 - scanpy/tests/test_plotting.py | 3 ++- 4 files changed, 2 insertions(+), 13 deletions(-) diff --git a/scanpy/plotting/_dotplot.py b/scanpy/plotting/_dotplot.py index 5eeccc8345..de1ef06c41 100644 --- a/scanpy/plotting/_dotplot.py +++ b/scanpy/plotting/_dotplot.py @@ -699,16 +699,7 @@ def _dotplot( size = frac ** size_exponent # rescale size to match smallest_dot and largest_dot size = size * (largest_dot - smallest_dot) + smallest_dot -<<<<<<< HEAD normalize = check_colornorm(vmin, vmax, vcenter, norm) -======= - - import matplotlib.colors - - normalize = matplotlib.colors.Normalize( - vmin=kwds.get('vmin'), vmax=kwds.get('vmax') - ) ->>>>>>> 617168f7 (address review) if color_on == 'square': if edge_color is None: diff --git a/scanpy/tests/external/test_scrublet.py b/scanpy/tests/external/test_scrublet.py index bbdf747b14..e09a3702c0 100644 --- a/scanpy/tests/external/test_scrublet.py +++ b/scanpy/tests/external/test_scrublet.py @@ -34,8 +34,6 @@ def test_scrublet_dense(): adata = sc.datasets.paul15()[:500].copy() sce.pp.scrublet(adata, use_approx_neighbors=False) - errors = [] - # replace assertions by conditions assert "predicted_doublet" in adata.obs.columns assert "doublet_score" in adata.obs.columns diff --git a/scanpy/tests/helpers.py b/scanpy/tests/helpers.py index f6bb214550..61fc35e23e 100644 --- a/scanpy/tests/helpers.py +++ b/scanpy/tests/helpers.py @@ -37,7 +37,6 @@ def check_rep_mutation(func, X, *, fields=["layer", "obsm"], **kwargs): ) np.testing.assert_array_equal(asarray(adata_X.X), result_array) - # Unmodified fields for field in fields: np.testing.assert_array_equal(X_array, asarray(adatas_proc[field].X)) diff --git a/scanpy/tests/test_plotting.py b/scanpy/tests/test_plotting.py index b83d40caa3..71e71926ea 100644 --- a/scanpy/tests/test_plotting.py +++ b/scanpy/tests/test_plotting.py @@ -915,7 +915,8 @@ def test_scatter_embedding_add_outline_vmin_vmax_norm(image_comparer, check_same ) save_and_compare_images('master_embedding_outline_vmin_vmax') - import matplotlib as mpl, matplotlib.pyplot as plt + import matplotlib as mpl + import matplotlib.pyplot as plt norm = mpl.colors.LogNorm() with pytest.raises( From e030ab1aac2f1945e80f3c783fa0e3457393bb27 Mon Sep 17 00:00:00 2001 From: Zethson Date: Mon, 15 Mar 2021 12:06:07 +0100 Subject: [PATCH 46/85] rebasing Signed-off-by: Zethson --- scanpy/plotting/_matrixplot.py | 8 -------- scanpy/plotting/_stacked_violin.py | 6 ------ scanpy/plotting/_tools/scatterplots.py | 14 -------------- scanpy/plotting/_utils.py | 4 ---- 4 files changed, 32 deletions(-) diff --git a/scanpy/plotting/_matrixplot.py b/scanpy/plotting/_matrixplot.py index ee71b82f0d..99a23b2300 100644 --- a/scanpy/plotting/_matrixplot.py +++ b/scanpy/plotting/_matrixplot.py @@ -215,19 +215,11 @@ def _mainplot(self, ax): cmap = pl.get_cmap(self.kwds.get('cmap', self.cmap)) if 'cmap' in self.kwds: del self.kwds['cmap'] -<<<<<<< HEAD normalize = check_colornorm( self.vboundnorm.vmin, self.vboundnorm.vmax, self.vboundnorm.vcenter, self.vboundnorm.norm, -======= - - import matplotlib.colors - - normalize = matplotlib.colors.Normalize( - vmin=self.kwds.get('vmin'), vmax=self.kwds.get('vmax') ->>>>>>> 617168f7 (address review) ) for axis in ['top', 'bottom', 'left', 'right']: diff --git a/scanpy/plotting/_stacked_violin.py b/scanpy/plotting/_stacked_violin.py index e364939e8a..347c8cc569 100644 --- a/scanpy/plotting/_stacked_violin.py +++ b/scanpy/plotting/_stacked_violin.py @@ -326,12 +326,6 @@ def _mainplot(self, ax): if self.are_axes_swapped: _color_df = _color_df.T -<<<<<<< HEAD -======= - norm = matplotlib.colors.Normalize( - vmin=self.kwds.get('vmin'), vmax=self.kwds.get('vmax') - ) ->>>>>>> 617168f7 (address review) cmap = pl.get_cmap(self.kwds.get('cmap', self.cmap)) if 'cmap' in self.kwds: del self.kwds['cmap'] diff --git a/scanpy/plotting/_tools/scatterplots.py b/scanpy/plotting/_tools/scatterplots.py index 154f638f05..154ffb7a91 100644 --- a/scanpy/plotting/_tools/scatterplots.py +++ b/scanpy/plotting/_tools/scatterplots.py @@ -294,7 +294,6 @@ def embedding( ) ax.set_title(value_to_plot) -<<<<<<< HEAD if not categorical: vmin_float, vmax_float, vcenter_float, norm_obj = _get_vboundnorm( vmin, vmax, vcenter, norm, count, color_vector @@ -307,19 +306,6 @@ def embedding( ) else: normalize = None -======= - # check vmin and vmax options - if categorical: - kwargs['vmin'] = kwargs['vmax'] = None - else: -<<<<<<< HEAD - kwargs['vmin'], kwargs['vmax'] = _get_vmin_vmax(vmin, vmax, count, color_vector) ->>>>>>> 40dc2c3b (add flake8 pre-commit) -======= - kwargs['vmin'], kwargs['vmax'] = _get_vmin_vmax( - vmin, vmax, count, color_vector - ) ->>>>>>> 617168f7 (address review) # make the scatter plot if projection == '3d': diff --git a/scanpy/plotting/_utils.py b/scanpy/plotting/_utils.py index f99741d3f3..dddfaf8dd4 100644 --- a/scanpy/plotting/_utils.py +++ b/scanpy/plotting/_utils.py @@ -1,7 +1,6 @@ import warnings import collections.abc as cabc from abc import ABC -from functools import lru_cache from typing import Union, List, Sequence, Tuple, Collection, Optional, Callable import anndata @@ -31,10 +30,7 @@ _FontSize = Literal[ 'xx-small', 'x-small', 'small', 'medium', 'large', 'x-large', 'xx-large' ] -<<<<<<< HEAD VBound = Union[str, float, Callable[[Sequence[float]], float]] -======= ->>>>>>> 617168f7 (address review) class _AxesSubplot(Axes, axes.SubplotBase, ABC): From 38e56247253817ea75bd9e780f10fed38e67f301 Mon Sep 17 00:00:00 2001 From: Zethson Date: Mon, 15 Mar 2021 12:19:35 +0100 Subject: [PATCH 47/85] rebasing Signed-off-by: Zethson --- scanpy/external/pp/_scvi.py | 210 ------------------------------------ scanpy/plotting/_utils.py | 14 +-- scanpy/tests/conftest.py | 1 - 3 files changed, 7 insertions(+), 218 deletions(-) delete mode 100644 scanpy/external/pp/_scvi.py diff --git a/scanpy/external/pp/_scvi.py b/scanpy/external/pp/_scvi.py deleted file mode 100644 index f4f75b0607..0000000000 --- a/scanpy/external/pp/_scvi.py +++ /dev/null @@ -1,210 +0,0 @@ -import warnings -import numpy as np -import pandas as pd -import scipy as sp - -from typing import Optional, Sequence, Union -from anndata import AnnData - -MIN_VERSION = "0.6.7" - - -def scvi( - adata: AnnData, - n_hidden: int = 128, - n_latent: int = 10, - n_layers: int = 1, - dispersion: str = "gene", - n_epochs: int = 400, - lr: int = 1e-3, - train_size: int = 1.0, - batch_key: Optional[str] = None, - use_highly_variable_genes: bool = True, - subset_genes: Optional[Sequence[Union[int, str]]] = None, - linear_decoder: bool = False, - copy: bool = False, - use_cuda: bool = True, - return_posterior: bool = True, - trainer_kwargs: dict = {}, - model_kwargs: dict = {}, -) -> Optional[AnnData]: - """\ - SCVI [Lopez18]_. - - Fits scVI model onto raw count data given an anndata object - - scVI uses stochastic optimization and deep neural networks to aggregate information - across similar cells and genes and to approximate the distributions that underlie - observed expression values, while accounting for batch effects and limited sensitivity. - - To use a linear-decoded Variational AutoEncoder model (implementation of [Svensson20]_.), - set linear_decoded = True. Compared to standard VAE, this model is less powerful, but can - be used to inspect which genes contribute to variation in the dataset. It may also be used - for all scVI tasks, like differential expression, batch correction, imputation, etc. - However, batch correction may be less powerful as it assumes a linear model. - - .. note:: - More information and bug reports `here `__. - - Parameters - ---------- - adata - An anndata file with `X` attribute of unnormalized count data - n_hidden - Number of nodes per hidden layer - n_latent - Dimensionality of the latent space - n_layers - Number of hidden layers used for encoder and decoder NNs - dispersion - One of the following - * `'gene'` - dispersion parameter of NB is constant per gene across cells - * `'gene-batch'` - dispersion can differ between different batches - * `'gene-label'` - dispersion can differ between different labels - * `'gene-cell'` - dispersion can differ for every gene in every cell - n_epochs - Number of epochs to train - lr - Learning rate - train_size - The train size, either a float between 0 and 1 or an integer for the number of training samples to use - batch_key - Column name in anndata.obs for batches. - If None, no batch correction is performed - If not None, batch correction is performed per batch category - use_highly_variable_genes - If true, uses only the genes in anndata.var["highly_variable"] - subset_genes - Optional list of indices or gene names to subset anndata. - If not None, use_highly_variable_genes is ignored - linear_decoder - If true, uses LDVAE model, which is an implementation of [Svensson20]_. - copy - If true, a copy of anndata is returned - return_posterior - If true, posterior object is returned - use_cuda - If true, uses cuda - trainer_kwargs - Extra arguments for UnsupervisedTrainer - model_kwargs - Extra arguments for VAE or LDVAE model - - Returns - ------- - If `copy` is true, anndata is returned. - If `return_posterior` is true, the posterior object is returned - If both `copy` and `return_posterior` are true, - a tuple of anndata and the posterior are returned in that order. - - `adata.obsm['X_scvi']` stores the latent representations - `adata.obsm['X_scvi_denoised']` stores the normalized mean of the negative binomial - `adata.obsm['X_scvi_sample_rate']` stores the mean of the negative binomial - - If linear_decoder is true: - `adata.uns['ldvae_loadings']` stores the per-gene weights in the linear decoder as a - genes by n_latent matrix. - - """ - warnings.warn( - "scvi via scanpy external API is no longer supported. " - + "Please use the new scvi-tools package from `scvi-tools.org`", - FutureWarning, - ) - - try: - from scvi.models import VAE, LDVAE - from scvi.inference import UnsupervisedTrainer - from scvi.dataset import AnnDatasetFromAnnData - except ImportError: - raise ImportError( - "Please install scvi package from https://github.com/YosefLab/scVI" - ) - - # check if observations are unnormalized using first 10 - # code from: https://github.com/theislab/dca/blob/89eee4ed01dd969b3d46e0c815382806fbfc2526/dca/io.py#L63-L69 - if len(adata) > 10: - X_subset = adata.X[:10] - else: - X_subset = adata.X - norm_error = ( - 'Make sure that the dataset (adata.X) contains unnormalized count data.' - ) - if sp.sparse.issparse(X_subset): - assert (X_subset.astype(int) != X_subset).nnz == 0, norm_error - else: - assert np.all(X_subset.astype(int) == X_subset), norm_error - - if subset_genes is not None: - adata_subset = adata[:, subset_genes] - elif use_highly_variable_genes and "highly_variable" in adata.var: - adata_subset = adata[:, adata.var["highly_variable"]] - else: - adata_subset = adata - - if batch_key is not None: - codes, uniques = pd.factorize(adata_subset.obs[batch_key]) - adata_subset.obs['_tmp_scvi_batch'] = codes - n_batches = len(uniques) - else: - n_batches = 0 - - dataset = AnnDatasetFromAnnData(adata_subset.copy(), batch_label='_tmp_scvi_batch') - - if linear_decoder: - vae = LDVAE( - n_input=dataset.nb_genes, - n_batch=n_batches, - n_labels=dataset.n_labels, - n_hidden=n_hidden, - n_latent=n_latent, - n_layers_encoder=n_layers, - dispersion=dispersion, - **model_kwargs, - ) - - else: - vae = VAE( - dataset.nb_genes, - n_batch=n_batches, - n_labels=dataset.n_labels, - n_hidden=n_hidden, - n_latent=n_latent, - n_layers=n_layers, - dispersion=dispersion, - **model_kwargs, - ) - - trainer = UnsupervisedTrainer( - model=vae, - gene_dataset=dataset, - use_cuda=use_cuda, - train_size=train_size, - **trainer_kwargs, - ) - - trainer.train(n_epochs=n_epochs, lr=lr) - - full = trainer.create_posterior( - trainer.model, dataset, indices=np.arange(len(dataset)) - ) - latent, batch_indices, labels = full.sequential().get_latent() - - if copy: - adata = adata.copy() - - adata.obsm['X_scvi'] = latent - adata.obsm['X_scvi_denoised'] = full.sequential().get_sample_scale() - adata.obsm['X_scvi_sample_rate'] = full.sequential().imputation() - - if linear_decoder: - loadings = vae.get_loadings() - df = pd.DataFrame(loadings, index=adata_subset.var_names) - adata.uns['ldvae_loadings'] = df - - if copy and return_posterior: - return adata, full - elif copy: - return adata - elif return_posterior: - return full diff --git a/scanpy/plotting/_utils.py b/scanpy/plotting/_utils.py index dddfaf8dd4..77187bdf10 100644 --- a/scanpy/plotting/_utils.py +++ b/scanpy/plotting/_utils.py @@ -1,6 +1,7 @@ import warnings import collections.abc as cabc from abc import ABC +from functools import lru_cache from typing import Union, List, Sequence, Tuple, Collection, Optional, Callable import anndata @@ -126,7 +127,7 @@ def timeseries_subplot( else: levels, _ = np.unique(color, return_inverse=True) colors = np.array(palette[: len(levels)].by_key()['color']) - subsets = [(x_range[color == level], X[color == level, :]) for level in levels] + subsets = [(x_range[color == l], X[color == l, :]) for l in levels] if ax is None: ax = pl.subplot() @@ -619,7 +620,6 @@ def setup_axes( figure_width = width_without_offsets + left_offset + right_offset draw_region_width_frac = draw_region_width / figure_width left_offset_frac = left_offset / figure_width - right_offset_frac = 1 - (len(panels) - 1) * left_offset_frac # noqa: F841 if ax is None: pl.figure( @@ -707,9 +707,7 @@ def scatter_base( ) for icolor, color in enumerate(colors): ax = axs[icolor] - left = panel_pos[2][2 * icolor] bottom = panel_pos[0][0] - width = draw_region_width / figure_width height = panel_pos[1][0] - bottom Y_sort = Y if not is_color_like(color) and sort_order: @@ -742,7 +740,9 @@ def scatter_base( rectangle = [left, bottom, width, height] fig = pl.gcf() ax_cb = fig.add_axes(rectangle) - pl.colorbar(sct, format=ticker.FuncFormatter(ticks_formatter), cax=ax_cb) + _ = pl.colorbar( + sct, format=ticker.FuncFormatter(ticks_formatter), cax=ax_cb + ) # set the title if title is not None: ax.set_title(title[icolor]) @@ -937,8 +937,8 @@ def make_pos(pos, node=root, currentLevel=0, parent=None, vert_loc=0): if levels is None: levels = make_levels({}) else: - levels = {level: {TOTAL: levels[level], CURRENT: 0} for level in levels} - vert_gap = height / (max([level for level in levels]) + 1) + levels = {l: {TOTAL: levels[l], CURRENT: 0} for l in levels} + vert_gap = height / (max([l for l in levels]) + 1) return make_pos({}) diff --git a/scanpy/tests/conftest.py b/scanpy/tests/conftest.py index 60213c236f..574dbf3543 100644 --- a/scanpy/tests/conftest.py +++ b/scanpy/tests/conftest.py @@ -11,7 +11,6 @@ import scanpy - scanpy.settings.verbosity = "hint" # define this after importing scanpy but before running tests From 7529cd3b6c6c527e741f5a3e2cfcef52f51e08b9 Mon Sep 17 00:00:00 2001 From: Zethson Date: Mon, 15 Mar 2021 12:25:55 +0100 Subject: [PATCH 48/85] add flake8 to dev docs Signed-off-by: Zethson --- docs/dev/code.rst | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/dev/code.rst b/docs/dev/code.rst index 218e73b4ae..c1eaf86d37 100644 --- a/docs/dev/code.rst +++ b/docs/dev/code.rst @@ -16,7 +16,13 @@ Code style ---------- New code should follow -`Black `__ -and Scanpy’s +`Black `__, +and +`flake8 `__. +We ignore a couple of flake8 checks which are documented in the .flake8 file in the root +of this repository. +To learn how to ignore checks per line please read +`flake8 violations `__. +Additionally, we use Scanpy’s `EditorConfig `__, so using an editor/IDE with support for both is helpful. From c7b9ee4c018e86b1c865cc411fec2768312fc316 Mon Sep 17 00:00:00 2001 From: Zethson Date: Mon, 15 Mar 2021 12:32:50 +0100 Subject: [PATCH 49/85] add autopep8 to pre-commits Signed-off-by: Zethson --- .pre-commit-config.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 861b71dbfc..8a6e70dc4b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,3 +7,7 @@ repos: rev: 3.8.4 hooks: - id: flake8 +- repo: https://github.com/pre-commit/mirrors-autopep8 + rev: v1.5.5 + hooks: + - id: autopep8 From ad388700b3ded29d39fc8df00bec0b2bb0be0917 Mon Sep 17 00:00:00 2001 From: Zethson Date: Mon, 15 Mar 2021 12:58:18 +0100 Subject: [PATCH 50/85] add flake8 ignore docs Signed-off-by: Zethson --- .flake8 | 10 ++++- .pre-commit-config.yaml | 4 -- scanpy/neighbors/__init__.py | 24 ++++++------ scanpy/plotting/_utils.py | 6 +-- .../_deprecated/highly_variable_genes.py | 6 ++- .../preprocessing/_highly_variable_genes.py | 2 +- scanpy/tests/conftest.py | 9 ++--- .../notebooks/test_paga_paul15_subsampled.py | 3 +- scanpy/tests/notebooks/test_pbmc3k.py | 3 +- scanpy/tests/test_logging.py | 38 +++++++++---------- scanpy/tests/test_plotting.py | 17 ++++----- 11 files changed, 61 insertions(+), 61 deletions(-) diff --git a/.flake8 b/.flake8 index 636b14206b..d69b5a9aaf 100644 --- a/.flake8 +++ b/.flake8 @@ -2,4 +2,12 @@ [flake8] max-line-length = 88 # switched off since they conflict with black's standards -ignore = F401, W503, E501, E203, E231, W504, E402, E126, E712, E741, E266, E262 +ignore = F401, # module imported but unused -> required for Scanpys API + W503, # line break before a binary operator -> black does not adhere to PEP8 + W504, # line break occured after a binary operator -> black does not adhere to PEP8 + E501, # line too long -> we accept long comment lines; black gets rid of long code lines + E203, # whitespace before : -> black does not adhere to PEP8 + E231, # missing whitespace after ,', ';', or ':' -> black does not adhere to PEP8 + E402, # module level import not at top of file -> required to circumvent circular imports for Scanpys API + E126, # continuation line over-indented for hanging indent -> black does not adhere to PEP8 + E266, # E266 too many leading '#' for block comment -> Scanpy allows them for comments into sections diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8a6e70dc4b..861b71dbfc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,7 +7,3 @@ repos: rev: 3.8.4 hooks: - id: flake8 -- repo: https://github.com/pre-commit/mirrors-autopep8 - rev: v1.5.5 - hooks: - - id: autopep8 diff --git a/scanpy/neighbors/__init__.py b/scanpy/neighbors/__init__.py index 6faf932e20..8ef371c853 100644 --- a/scanpy/neighbors/__init__.py +++ b/scanpy/neighbors/__init__.py @@ -769,9 +769,7 @@ def compute_neighbors( self.knn = knn X = _choose_representation(self._adata, use_rep=use_rep, n_pcs=n_pcs) # neighbor search - use_dense_distances = ( - metric == 'euclidean' and X.shape[0] < 8192 - ) or knn == False + use_dense_distances = (metric == 'euclidean' and X.shape[0] < 8192) or not knn if use_dense_distances: _distances = pairwise_distances(X, metric=metric, **metric_kwds) knn_indices, knn_distances = _get_indices_distances_from_dense_matrix( @@ -862,7 +860,7 @@ def _compute_connectivities_diffmap(self, density_normalize=True): # make the weight matrix sparse if not self.knn: mask = W > 1e-14 - W[mask == False] = 0 + W[mask == False] = 0 # noqa: E712 else: # restrict number of neighbors to ~k # build a symmetric mask @@ -874,7 +872,7 @@ def _compute_connectivities_diffmap(self, density_normalize=True): W[j, i] = W[i, j] mask[j, i] = True # set all entries that are not nearest neighbors to zero - W[mask == False] = 0 + W[mask == False] = 0 # noqa: E712 else: W = ( Dsq.copy() @@ -1025,14 +1023,14 @@ def _get_dpt_row(self, i): mask = self._connected_components[1] == label row = sum( ( - self.eigen_values[l] - / (1 - self.eigen_values[l]) - * (self.eigen_basis[i, l] - self.eigen_basis[:, l]) + self.eigen_values[k] + / (1 - self.eigen_values[k]) + * (self.eigen_basis[k, k] - self.eigen_basis[:, k]) ) ** 2 # account for float32 precision - for l in range(0, self.eigen_values.size) - if self.eigen_values[l] < 0.9994 + for k in range(0, self.eigen_values.size) + if self.eigen_values[k] < 0.9994 ) # thanks to Marius Lange for pointing Alex to this: # we will likely remove the contributions from the stationary state below when making @@ -1040,9 +1038,9 @@ def _get_dpt_row(self, i): # they never seem to have deteriorated results, but also other distance measures (see e.g. # PAGA paper) don't have it, which makes sense row += sum( - (self.eigen_basis[i, l] - self.eigen_basis[:, l]) ** 2 - for l in range(0, self.eigen_values.size) - if self.eigen_values[l] >= 0.9994 + (self.eigen_basis[i, k] - self.eigen_basis[:, k]) ** 2 + for k in range(0, self.eigen_values.size) + if self.eigen_values[k] >= 0.9994 ) if mask is not None: row[~mask] = np.inf diff --git a/scanpy/plotting/_utils.py b/scanpy/plotting/_utils.py index 77187bdf10..112ea36a7b 100644 --- a/scanpy/plotting/_utils.py +++ b/scanpy/plotting/_utils.py @@ -127,7 +127,7 @@ def timeseries_subplot( else: levels, _ = np.unique(color, return_inverse=True) colors = np.array(palette[: len(levels)].by_key()['color']) - subsets = [(x_range[color == l], X[color == l, :]) for l in levels] + subsets = [(x_range[color == level], X[color == level, :]) for level in levels] if ax is None: ax = pl.subplot() @@ -937,8 +937,8 @@ def make_pos(pos, node=root, currentLevel=0, parent=None, vert_loc=0): if levels is None: levels = make_levels({}) else: - levels = {l: {TOTAL: levels[l], CURRENT: 0} for l in levels} - vert_gap = height / (max([l for l in levels]) + 1) + levels = {level: {TOTAL: levels[level], CURRENT: 0} for level in levels} + vert_gap = height / (max([level for level in levels]) + 1) return make_pos({}) diff --git a/scanpy/preprocessing/_deprecated/highly_variable_genes.py b/scanpy/preprocessing/_deprecated/highly_variable_genes.py index 570a5f9f25..9b4f834512 100644 --- a/scanpy/preprocessing/_deprecated/highly_variable_genes.py +++ b/scanpy/preprocessing/_deprecated/highly_variable_genes.py @@ -170,8 +170,10 @@ def filter_genes_dispersion( disp_mean_bin[one_gene_per_bin] = 0 # actually do the normalization df['dispersion_norm'] = ( - df['dispersion'].values # use values here as index differs - - disp_mean_bin[df['mean_bin'].values].values + df['dispersion'].values + - disp_mean_bin[ # use values here as index differs + df['mean_bin'].values + ].values ) / disp_std_bin[df['mean_bin'].values].values elif flavor == 'cell_ranger': from statsmodels import robust diff --git a/scanpy/preprocessing/_highly_variable_genes.py b/scanpy/preprocessing/_highly_variable_genes.py index 039a57cb58..796405e0c0 100644 --- a/scanpy/preprocessing/_highly_variable_genes.py +++ b/scanpy/preprocessing/_highly_variable_genes.py @@ -57,7 +57,7 @@ def _highly_variable_genes_seurat_v3( ) X = adata.layers[layer] if layer is not None else adata.X - if check_values and (check_nonnegative_integers(X) == False): + if check_values and not check_nonnegative_integers(X): warnings.warn( "`flavor='seurat_v3'` expects raw count data, but non-integers were found.", UserWarning, diff --git a/scanpy/tests/conftest.py b/scanpy/tests/conftest.py index 574dbf3543..163cabeba0 100644 --- a/scanpy/tests/conftest.py +++ b/scanpy/tests/conftest.py @@ -1,14 +1,13 @@ +import scanpy +import pytest +from matplotlib.testing.compare import compare_images, make_test_filename +from matplotlib import pyplot import sys from pathlib import Path import matplotlib as mpl mpl.use('agg') -from matplotlib import pyplot -from matplotlib.testing.compare import compare_images, make_test_filename -import pytest - -import scanpy scanpy.settings.verbosity = "hint" diff --git a/scanpy/tests/notebooks/test_paga_paul15_subsampled.py b/scanpy/tests/notebooks/test_paga_paul15_subsampled.py index 839d93f40a..9ba4504cf1 100644 --- a/scanpy/tests/notebooks/test_paga_paul15_subsampled.py +++ b/scanpy/tests/notebooks/test_paga_paul15_subsampled.py @@ -3,6 +3,7 @@ # # This is the subsampled notebook for testing. +import scanpy as sc from pathlib import Path import numpy as np @@ -10,8 +11,6 @@ setup() -import scanpy as sc - HERE: Path = Path(__file__).parent ROOT = HERE / '_images_paga_paul15_subsampled' diff --git a/scanpy/tests/notebooks/test_pbmc3k.py b/scanpy/tests/notebooks/test_pbmc3k.py index 0146c4b4de..9b8a3ec4c5 100644 --- a/scanpy/tests/notebooks/test_pbmc3k.py +++ b/scanpy/tests/notebooks/test_pbmc3k.py @@ -10,6 +10,7 @@ # ([here](http://cf.10xgenomics.com/samples/cell-exp/1.1.0/pbmc3k/pbmc3k_filtered_gene_bc_matrices.tar.gz) # from this [webpage](https://support.10xgenomics.com/single-cell-gene-expression/datasets/1.1.0/pbmc3k)). +import scanpy as sc from pathlib import Path import numpy as np @@ -19,8 +20,6 @@ setup() -import scanpy as sc - HERE: Path = Path(__file__).parent ROOT = HERE / 'pbmc3k_images' diff --git a/scanpy/tests/test_logging.py b/scanpy/tests/test_logging.py index a954d85421..0da37040bc 100644 --- a/scanpy/tests/test_logging.py +++ b/scanpy/tests/test_logging.py @@ -5,7 +5,7 @@ import pytest -from scanpy import Verbosity, settings as s, logging as l +from scanpy import Verbosity, settings as s, logging as log import scanpy as sc @@ -24,29 +24,29 @@ def test_defaults(): def test_formats(capsys, logging_state): s.logfile = sys.stderr s.verbosity = Verbosity.debug - l.error('0') + log.error('0') assert capsys.readouterr().err == 'ERROR: 0\n' - l.warning('1') + log.warning('1') assert capsys.readouterr().err == 'WARNING: 1\n' - l.info('2') + log.info('2') assert capsys.readouterr().err == '2\n' - l.hint('3') + log.hint('3') assert capsys.readouterr().err == '--> 3\n' - l.debug('4') + log.debug('4') assert capsys.readouterr().err == ' 4\n' def test_deep(capsys, logging_state): s.logfile = sys.stderr s.verbosity = Verbosity.hint - l.hint('0') + log.hint('0') assert capsys.readouterr().err == '--> 0\n' - l.hint('1', deep='1!') + log.hint('1', deep='1!') assert capsys.readouterr().err == '--> 1\n' s.verbosity = Verbosity.debug - l.hint('2') + log.hint('2') assert capsys.readouterr().err == '--> 2\n' - l.hint('3', deep='3!') + log.hint('3', deep='3!') assert capsys.readouterr().err == '--> 3: 3!\n' @@ -57,15 +57,15 @@ def test_logfile(tmp_path, logging_state): s.logfile = io assert s.logfile is io assert s.logpath is None - l.error('test!') + log.error('test!') assert io.getvalue() == 'ERROR: test!\n' p = tmp_path / 'test.log' s.logpath = p assert s.logpath == p assert s.logfile.name == str(p) - l.hint('test2') - l.debug('invisible') + log.hint('test2') + log.debug('invisible') assert s.logpath.read_text() == '--> test2\n' @@ -80,18 +80,18 @@ def now(tz): counter += 1 return datetime(2000, 1, 1, second=counter, microsecond=counter, tzinfo=tz) - monkeypatch.setattr(l, 'datetime', IncTime) + monkeypatch.setattr(log, 'datetime', IncTime) s.verbosity = Verbosity.debug - l.hint('1') + log.hint('1') assert counter == 1 and capsys.readouterr().err == '--> 1\n' - start = l.info('2') + start = log.info('2') assert counter == 2 and capsys.readouterr().err == '2\n' - l.hint('3') + log.hint('3') assert counter == 3 and capsys.readouterr().err == '--> 3\n' - l.info('4', time=start) + log.info('4', time=start) assert counter == 4 and capsys.readouterr().err == '4 (0:00:02)\n' - l.info('5 {time_passed}', time=start) + log.info('5 {time_passed}', time=start) assert counter == 5 and capsys.readouterr().err == '5 0:00:03\n' diff --git a/scanpy/tests/test_plotting.py b/scanpy/tests/test_plotting.py index 71e71926ea..c32090927f 100644 --- a/scanpy/tests/test_plotting.py +++ b/scanpy/tests/test_plotting.py @@ -1,3 +1,11 @@ +import scanpy as sc +from anndata import AnnData +from matplotlib.testing.compare import compare_images +import pandas as pd +import numpy as np +import matplotlib.cm as cm +import matplotlib.pyplot as plt +import matplotlib as mpl from functools import partial from pathlib import Path from itertools import repeat, chain, combinations @@ -10,15 +18,6 @@ setup() -import matplotlib as mpl -import matplotlib.pyplot as plt -import matplotlib.cm as cm -import numpy as np -import pandas as pd -from matplotlib.testing.compare import compare_images -from anndata import AnnData - -import scanpy as sc HERE: Path = Path(__file__).parent ROOT = HERE / '_images' From c96824437ca08830154a68be12b23d203ca85968 Mon Sep 17 00:00:00 2001 From: Zethson Date: Mon, 15 Mar 2021 13:01:52 +0100 Subject: [PATCH 51/85] add exception todos Signed-off-by: Zethson --- scanpy/neighbors/__init__.py | 2 +- scanpy/plotting/_anndata.py | 2 +- scanpy/plotting/_tools/scatterplots.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/scanpy/neighbors/__init__.py b/scanpy/neighbors/__init__.py index 8ef371c853..c803eb663e 100644 --- a/scanpy/neighbors/__init__.py +++ b/scanpy/neighbors/__init__.py @@ -795,7 +795,7 @@ def compute_neighbors( try: if forest: self._rp_forest = _make_forest_dict(forest) - except Exception: + except Exception: # TODO catch the correct exception pass # write indices as attributes if write_knn_indices: diff --git a/scanpy/plotting/_anndata.py b/scanpy/plotting/_anndata.py index 6c319682ce..193d753c96 100755 --- a/scanpy/plotting/_anndata.py +++ b/scanpy/plotting/_anndata.py @@ -1993,7 +1993,7 @@ def _plot_gene_groups_brackets( va='bottom', rotation=rotation, ) - except Exception: + except Exception: # TODO catch the correct exception pass else: top = left diff --git a/scanpy/plotting/_tools/scatterplots.py b/scanpy/plotting/_tools/scatterplots.py index 154ffb7a91..b99af5c544 100644 --- a/scanpy/plotting/_tools/scatterplots.py +++ b/scanpy/plotting/_tools/scatterplots.py @@ -994,7 +994,7 @@ def _get_data_points( data_points = [] for comp in components_list: data_points.append(adata.obsm[basis_key][:, comp]) - except Exception: + except Exception: # TODO catch the correct exception raise ValueError( "Given components: '{}' are not valid. Please check. " "A valid example is `components='2,3'`" From 83e31cfc2657e83693e54408b3f3fbb65f86db1b Mon Sep 17 00:00:00 2001 From: Zethson Date: Mon, 15 Mar 2021 13:06:01 +0100 Subject: [PATCH 52/85] add ignore directories Signed-off-by: Zethson --- .flake8 | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.flake8 b/.flake8 index d69b5a9aaf..61a5be1fad 100644 --- a/.flake8 +++ b/.flake8 @@ -11,3 +11,10 @@ ignore = F401, # module imported but unused -> required for Scanpys API E402, # module level import not at top of file -> required to circumvent circular imports for Scanpys API E126, # continuation line over-indented for hanging indent -> black does not adhere to PEP8 E266, # E266 too many leading '#' for block comment -> Scanpy allows them for comments into sections + E731 # Do not assign a lambda expression, use a def -> Scanpy allows lambda expression assignments +exclude = + .git, + __pycache__, + build, + docs/_build + dist From f8b6b705447d10a4a43313e59b223fffd471da46 Mon Sep 17 00:00:00 2001 From: Zethson Date: Mon, 15 Mar 2021 13:09:10 +0100 Subject: [PATCH 53/85] reinstated lambdas Signed-off-by: Zethson --- scanpy/tools/_rank_genes_groups.py | 34 +++++++++--------------------- 1 file changed, 10 insertions(+), 24 deletions(-) diff --git a/scanpy/tools/_rank_genes_groups.py b/scanpy/tools/_rank_genes_groups.py index 99393f691c..06b919b4b2 100644 --- a/scanpy/tools/_rank_genes_groups.py +++ b/scanpy/tools/_rank_genes_groups.py @@ -35,32 +35,24 @@ def _ranks(X, mask=None, mask_rest=None): n_genes = X.shape[1] if issparse(X): - - def merge(tpl): - return vstack(tpl).toarray() - - def adapt(X): - return X.toarray() + merge = lambda tpl: vstack(tpl).toarray() + adapt = lambda X: X.toarray() else: merge = np.vstack - - def adapt(X): - return X + adapt = lambda X: X masked = mask is not None and mask_rest is not None if masked: n_cells = np.count_nonzero(mask) + np.count_nonzero(mask_rest) - - def get_chunk(X, left, right): - return merge((X[mask, left:right], X[mask_rest, left:right])) + get_chunk = lambda X, left, right: merge( + (X[mask, left:right], X[mask_rest, left:right]) + ) else: n_cells = X.shape[0] - - def get_chunk(X, left, right): - return adapt(X[:, left:right]) + get_chunk = lambda X, left, right: adapt(X[:, left:right]) # Calculate chunk frames max_chunk = floor(CONST_MAX_SIZE / n_cells) @@ -178,14 +170,10 @@ def _basic_stats(self): del X_rest if issparse(self.X): - - def get_nonzeros(X): - return X.getnnz(axis=0) + get_nonzeros = lambda X: X.getnnz(axis=0) else: - - def get_nonzeros(X): - return np.count_nonzero(X, axis=0) + get_nonzeros = lambda X: np.count_nonzero(X, axis=0) for imask, mask in enumerate(self.groups_masks): X_mask = self.X[mask] @@ -755,9 +743,7 @@ def filter_rank_genes_groups( ) if 'log1p' in adata.uns_keys() and adata.uns['log1p']['base'] is not None: - - def expm1_func(x): - return np.expm1(x * np.log(adata.uns['log1p']['base'])) + expm1_func = lambda x: np.expm1(x * np.log(adata.uns['log1p']['base'])) else: expm1_func = np.expm1 From 9e6722ac040d10b1b04b2db22c7353c1d61ca097 Mon Sep 17 00:00:00 2001 From: Zethson Date: Mon, 15 Mar 2021 13:25:05 +0100 Subject: [PATCH 54/85] fix tests Signed-off-by: Zethson --- scanpy/tests/external/test_scrublet.py | 20 -------------------- scanpy/tests/test_pca.py | 1 + 2 files changed, 1 insertion(+), 20 deletions(-) diff --git a/scanpy/tests/external/test_scrublet.py b/scanpy/tests/external/test_scrublet.py index 4e09d0a60e..e09a3702c0 100644 --- a/scanpy/tests/external/test_scrublet.py +++ b/scanpy/tests/external/test_scrublet.py @@ -41,26 +41,6 @@ def test_scrublet_dense(): assert adata.obs["predicted_doublet"].any(), "Expect some doublets to be identified" -def test_scrublet_dense(): - """ - Test that Scrublet works for dense matrices. - - Check that scrublet runs and detects some doublets when a dense matrix is supplied. - """ - pytest.importorskip("scrublet") - - adata = sc.datasets.paul15()[:500].copy() - sce.pp.scrublet(adata, use_approx_neighbors=False) - - errors = [] - - # replace assertions by conditions - assert "predicted_doublet" in adata.obs.columns - assert "doublet_score" in adata.obs.columns - - assert adata.obs["predicted_doublet"].any(), "Expect some doublets to be identified" - - def test_scrublet_params(): """ Test that Scrublet args are passed. diff --git a/scanpy/tests/test_pca.py b/scanpy/tests/test_pca.py index d4feac5562..975f7ee34d 100644 --- a/scanpy/tests/test_pca.py +++ b/scanpy/tests/test_pca.py @@ -3,6 +3,7 @@ from anndata import AnnData import scanpy as sc +from scanpy.tests.fixtures import array_type, float_dtype from anndata.tests.helpers import assert_equal A_list = [ From 207f650697a52c8e7557291f769e75f404d9eb92 Mon Sep 17 00:00:00 2001 From: Zethson Date: Mon, 15 Mar 2021 13:27:12 +0100 Subject: [PATCH 55/85] fix tests Signed-off-by: Zethson --- scanpy/plotting/_tools/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/scanpy/plotting/_tools/__init__.py b/scanpy/plotting/_tools/__init__.py index 1be61d2a0e..d3d8b87e4d 100644 --- a/scanpy/plotting/_tools/__init__.py +++ b/scanpy/plotting/_tools/__init__.py @@ -165,7 +165,6 @@ def dpt_timeseries( ): """\ Heatmap of pseudotime series. - Parameters ---------- as_heatmap From 7fa610e77d5b74962e35fd858dd9478b3728e4c3 Mon Sep 17 00:00:00 2001 From: Zethson Date: Mon, 15 Mar 2021 13:51:23 +0100 Subject: [PATCH 56/85] fix tests Signed-off-by: Zethson --- scanpy/plotting/_utils.py | 3 +++ scanpy/tests/conftest.py | 8 ++++---- .../notebooks/test_paga_paul15_subsampled.py | 3 ++- scanpy/tests/notebooks/test_pbmc3k.py | 3 ++- scanpy/tests/test_plotting.py | 17 +++++++++-------- 5 files changed, 20 insertions(+), 14 deletions(-) diff --git a/scanpy/plotting/_utils.py b/scanpy/plotting/_utils.py index 112ea36a7b..4ad2f861e6 100644 --- a/scanpy/plotting/_utils.py +++ b/scanpy/plotting/_utils.py @@ -620,6 +620,7 @@ def setup_axes( figure_width = width_without_offsets + left_offset + right_offset draw_region_width_frac = draw_region_width / figure_width left_offset_frac = left_offset / figure_width + right_offset_frac = 1 - (len(panels) - 1) * left_offset_frac # noqa: F841 if ax is None: pl.figure( @@ -707,7 +708,9 @@ def scatter_base( ) for icolor, color in enumerate(colors): ax = axs[icolor] + left = panel_pos[2][2 * icolor] # noqa: F841 bottom = panel_pos[0][0] + width = draw_region_width / figure_width # noqa: F841 height = panel_pos[1][0] - bottom Y_sort = Y if not is_color_like(color) and sort_order: diff --git a/scanpy/tests/conftest.py b/scanpy/tests/conftest.py index 163cabeba0..d7abdda218 100644 --- a/scanpy/tests/conftest.py +++ b/scanpy/tests/conftest.py @@ -1,13 +1,13 @@ -import scanpy -import pytest -from matplotlib.testing.compare import compare_images, make_test_filename -from matplotlib import pyplot import sys from pathlib import Path import matplotlib as mpl mpl.use('agg') +import scanpy +import pytest +from matplotlib.testing.compare import compare_images, make_test_filename +from matplotlib import pyplot scanpy.settings.verbosity = "hint" diff --git a/scanpy/tests/notebooks/test_paga_paul15_subsampled.py b/scanpy/tests/notebooks/test_paga_paul15_subsampled.py index 9ba4504cf1..839d93f40a 100644 --- a/scanpy/tests/notebooks/test_paga_paul15_subsampled.py +++ b/scanpy/tests/notebooks/test_paga_paul15_subsampled.py @@ -3,7 +3,6 @@ # # This is the subsampled notebook for testing. -import scanpy as sc from pathlib import Path import numpy as np @@ -11,6 +10,8 @@ setup() +import scanpy as sc + HERE: Path = Path(__file__).parent ROOT = HERE / '_images_paga_paul15_subsampled' diff --git a/scanpy/tests/notebooks/test_pbmc3k.py b/scanpy/tests/notebooks/test_pbmc3k.py index 9b8a3ec4c5..0146c4b4de 100644 --- a/scanpy/tests/notebooks/test_pbmc3k.py +++ b/scanpy/tests/notebooks/test_pbmc3k.py @@ -10,7 +10,6 @@ # ([here](http://cf.10xgenomics.com/samples/cell-exp/1.1.0/pbmc3k/pbmc3k_filtered_gene_bc_matrices.tar.gz) # from this [webpage](https://support.10xgenomics.com/single-cell-gene-expression/datasets/1.1.0/pbmc3k)). -import scanpy as sc from pathlib import Path import numpy as np @@ -20,6 +19,8 @@ setup() +import scanpy as sc + HERE: Path = Path(__file__).parent ROOT = HERE / 'pbmc3k_images' diff --git a/scanpy/tests/test_plotting.py b/scanpy/tests/test_plotting.py index c32090927f..71e71926ea 100644 --- a/scanpy/tests/test_plotting.py +++ b/scanpy/tests/test_plotting.py @@ -1,11 +1,3 @@ -import scanpy as sc -from anndata import AnnData -from matplotlib.testing.compare import compare_images -import pandas as pd -import numpy as np -import matplotlib.cm as cm -import matplotlib.pyplot as plt -import matplotlib as mpl from functools import partial from pathlib import Path from itertools import repeat, chain, combinations @@ -18,6 +10,15 @@ setup() +import matplotlib as mpl +import matplotlib.pyplot as plt +import matplotlib.cm as cm +import numpy as np +import pandas as pd +from matplotlib.testing.compare import compare_images +from anndata import AnnData + +import scanpy as sc HERE: Path = Path(__file__).parent ROOT = HERE / '_images' From 976d825810bef4ca223f38080bb414a3b757eef2 Mon Sep 17 00:00:00 2001 From: Zethson Date: Mon, 15 Mar 2021 14:09:18 +0100 Subject: [PATCH 57/85] fix tests Signed-off-by: Zethson --- scanpy/tests/conftest.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/scanpy/tests/conftest.py b/scanpy/tests/conftest.py index d7abdda218..574dbf3543 100644 --- a/scanpy/tests/conftest.py +++ b/scanpy/tests/conftest.py @@ -4,10 +4,11 @@ import matplotlib as mpl mpl.use('agg') -import scanpy -import pytest -from matplotlib.testing.compare import compare_images, make_test_filename from matplotlib import pyplot +from matplotlib.testing.compare import compare_images, make_test_filename +import pytest + +import scanpy scanpy.settings.verbosity = "hint" From e3d916cb259db7315404e4e64af245859abb619c Mon Sep 17 00:00:00 2001 From: Zethson Date: Mon, 15 Mar 2021 14:13:06 +0100 Subject: [PATCH 58/85] fix tests Signed-off-by: Zethson --- scanpy/neighbors/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scanpy/neighbors/__init__.py b/scanpy/neighbors/__init__.py index c803eb663e..01258ba7e7 100644 --- a/scanpy/neighbors/__init__.py +++ b/scanpy/neighbors/__init__.py @@ -1025,7 +1025,7 @@ def _get_dpt_row(self, i): ( self.eigen_values[k] / (1 - self.eigen_values[k]) - * (self.eigen_basis[k, k] - self.eigen_basis[:, k]) + * (self.eigen_basis[i, k] - self.eigen_basis[:, k]) ) ** 2 # account for float32 precision From 5ca852718ea883f21e68a4495b6a671c8b5881fc Mon Sep 17 00:00:00 2001 From: Lukas Heumos Date: Tue, 16 Mar 2021 14:04:07 +0100 Subject: [PATCH 59/85] Add E741 to allowed flake8 violations. Co-authored-by: Isaac Virshup --- .flake8 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.flake8 b/.flake8 index 61a5be1fad..8dc2ac3aa5 100644 --- a/.flake8 +++ b/.flake8 @@ -11,7 +11,8 @@ ignore = F401, # module imported but unused -> required for Scanpys API E402, # module level import not at top of file -> required to circumvent circular imports for Scanpys API E126, # continuation line over-indented for hanging indent -> black does not adhere to PEP8 E266, # E266 too many leading '#' for block comment -> Scanpy allows them for comments into sections - E731 # Do not assign a lambda expression, use a def -> Scanpy allows lambda expression assignments + E731, # Do not assign a lambda expression, use a def -> Scanpy allows lambda expression assignments + E741, # allow I, O, l as variable names -> I is the identity matrix, i, j, k, l is reasonable indexing notation exclude = .git, __pycache__, From c8b727346a5785fb80d1a7d713c83d0a6232eab4 Mon Sep 17 00:00:00 2001 From: Lukas Heumos Date: Tue, 16 Mar 2021 14:05:22 +0100 Subject: [PATCH 60/85] Add F811 flake8 ignore for tests Co-authored-by: Isaac Virshup --- .flake8 | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.flake8 b/.flake8 index 8dc2ac3aa5..c828a06a22 100644 --- a/.flake8 +++ b/.flake8 @@ -17,5 +17,8 @@ exclude = .git, __pycache__, build, - docs/_build - dist + docs/_build, + dist, +per-file-ignores = + # F811 Redefinition of unused name from line, does not play nice with pytest fixtures + tests/test*.py: F811, From 9abc967d99883661b9be94fa3f8f5290287b3788 Mon Sep 17 00:00:00 2001 From: Lukas Heumos Date: Tue, 16 Mar 2021 14:05:50 +0100 Subject: [PATCH 61/85] Fix mask comparison Co-authored-by: Isaac Virshup --- scanpy/neighbors/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scanpy/neighbors/__init__.py b/scanpy/neighbors/__init__.py index 01258ba7e7..4f3ddd4833 100644 --- a/scanpy/neighbors/__init__.py +++ b/scanpy/neighbors/__init__.py @@ -860,7 +860,7 @@ def _compute_connectivities_diffmap(self, density_normalize=True): # make the weight matrix sparse if not self.knn: mask = W > 1e-14 - W[mask == False] = 0 # noqa: E712 + W[~mask] = 0 else: # restrict number of neighbors to ~k # build a symmetric mask From 3a8322892629c53630747bc841e27db50e217358 Mon Sep 17 00:00:00 2001 From: Lukas Heumos Date: Tue, 16 Mar 2021 14:06:45 +0100 Subject: [PATCH 62/85] Fix mask comparison Co-authored-by: Isaac Virshup --- scanpy/neighbors/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scanpy/neighbors/__init__.py b/scanpy/neighbors/__init__.py index 4f3ddd4833..17c821bfb2 100644 --- a/scanpy/neighbors/__init__.py +++ b/scanpy/neighbors/__init__.py @@ -872,7 +872,7 @@ def _compute_connectivities_diffmap(self, density_normalize=True): W[j, i] = W[i, j] mask[j, i] = True # set all entries that are not nearest neighbors to zero - W[mask == False] = 0 # noqa: E712 + W[~mask] = 0 else: W = ( Dsq.copy() From e2a4ce7e6fe1284c87a4adb2399a9f923bd783f0 Mon Sep 17 00:00:00 2001 From: Zethson Date: Tue, 16 Mar 2021 14:12:28 +0100 Subject: [PATCH 63/85] fix flake8 config file Signed-off-by: Zethson --- .flake8 | 36 +++++++++++++++++++----------- scanpy/neighbors/__init__.py | 10 ++++----- scanpy/plotting/_tools/__init__.py | 1 + 3 files changed, 29 insertions(+), 18 deletions(-) diff --git a/.flake8 b/.flake8 index c828a06a22..4089ff0b50 100644 --- a/.flake8 +++ b/.flake8 @@ -1,24 +1,34 @@ # Can't yet be moved to the pyproject.toml due to https://gitlab.com/pycqa/flake8/-/issues/428#note_251982786 [flake8] max-line-length = 88 -# switched off since they conflict with black's standards -ignore = F401, # module imported but unused -> required for Scanpys API - W503, # line break before a binary operator -> black does not adhere to PEP8 - W504, # line break occured after a binary operator -> black does not adhere to PEP8 - E501, # line too long -> we accept long comment lines; black gets rid of long code lines - E203, # whitespace before : -> black does not adhere to PEP8 - E231, # missing whitespace after ,', ';', or ':' -> black does not adhere to PEP8 - E402, # module level import not at top of file -> required to circumvent circular imports for Scanpys API - E126, # continuation line over-indented for hanging indent -> black does not adhere to PEP8 - E266, # E266 too many leading '#' for block comment -> Scanpy allows them for comments into sections - E731, # Do not assign a lambda expression, use a def -> Scanpy allows lambda expression assignments - E741, # allow I, O, l as variable names -> I is the identity matrix, i, j, k, l is reasonable indexing notation +ignore = # module imported but unused -> required for Scanpys API + F401, + # line break before a binary operator -> black does not adhere to PEP8 + W503, + # line break occured after a binary operator -> black does not adhere to PEP8 + W504, + # line too long -> we accept long comment lines; black gets rid of long code lines + E501, + # whitespace before : -> black does not adhere to PEP8 + E203, + # missing whitespace after ,', ';', or ':' -> black does not adhere to PEP8 + E231, + # module level import not at top of file -> required to circumvent circular imports for Scanpys API + E402, + # continuation line over-indented for hanging indent -> black does not adhere to PEP8 + E126, + # E266 too many leading '#' for block comment -> Scanpy allows them for comments into sections + E266 + # Do not assign a lambda expression, use a def -> Scanpy allows lambda expression assignments, + E731 + # allow I, O, l as variable names -> I is the identity matrix, i, j, k, l is reasonable indexing notation + E741, exclude = .git, __pycache__, build, docs/_build, dist, -per-file-ignores = +per-file-ignores = # F811 Redefinition of unused name from line, does not play nice with pytest fixtures tests/test*.py: F811, diff --git a/scanpy/neighbors/__init__.py b/scanpy/neighbors/__init__.py index 17c821bfb2..4caebbee8a 100644 --- a/scanpy/neighbors/__init__.py +++ b/scanpy/neighbors/__init__.py @@ -1023,14 +1023,14 @@ def _get_dpt_row(self, i): mask = self._connected_components[1] == label row = sum( ( - self.eigen_values[k] - / (1 - self.eigen_values[k]) - * (self.eigen_basis[i, k] - self.eigen_basis[:, k]) + self.eigen_values[j] + / (1 - self.eigen_values[j]) + * (self.eigen_basis[i, j] - self.eigen_basis[:, j]) ) ** 2 # account for float32 precision - for k in range(0, self.eigen_values.size) - if self.eigen_values[k] < 0.9994 + for j in range(0, self.eigen_values.size) + if self.eigen_values[j] < 0.9994 ) # thanks to Marius Lange for pointing Alex to this: # we will likely remove the contributions from the stationary state below when making diff --git a/scanpy/plotting/_tools/__init__.py b/scanpy/plotting/_tools/__init__.py index d3d8b87e4d..1be61d2a0e 100644 --- a/scanpy/plotting/_tools/__init__.py +++ b/scanpy/plotting/_tools/__init__.py @@ -165,6 +165,7 @@ def dpt_timeseries( ): """\ Heatmap of pseudotime series. + Parameters ---------- as_heatmap From 0c69d81ddaded894c2f0655e9bc6d7c363c4acbc Mon Sep 17 00:00:00 2001 From: Zethson Date: Tue, 16 Mar 2021 14:14:49 +0100 Subject: [PATCH 64/85] readded autopep8 Signed-off-by: Zethson --- .pre-commit-config.yaml | 5 +++++ scanpy/tools/_rank_genes_groups.py | 34 +++++++++++++++++++++--------- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 861b71dbfc..95a96ae781 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,3 +7,8 @@ repos: rev: 3.8.4 hooks: - id: flake8 +- repo: https://github.com/pre-commit/mirrors-autopep8 + rev: v1.5.5 + hooks: + - id: autopep8 + args: ["-i"] diff --git a/scanpy/tools/_rank_genes_groups.py b/scanpy/tools/_rank_genes_groups.py index 06b919b4b2..99393f691c 100644 --- a/scanpy/tools/_rank_genes_groups.py +++ b/scanpy/tools/_rank_genes_groups.py @@ -35,24 +35,32 @@ def _ranks(X, mask=None, mask_rest=None): n_genes = X.shape[1] if issparse(X): - merge = lambda tpl: vstack(tpl).toarray() - adapt = lambda X: X.toarray() + + def merge(tpl): + return vstack(tpl).toarray() + + def adapt(X): + return X.toarray() else: merge = np.vstack - adapt = lambda X: X + + def adapt(X): + return X masked = mask is not None and mask_rest is not None if masked: n_cells = np.count_nonzero(mask) + np.count_nonzero(mask_rest) - get_chunk = lambda X, left, right: merge( - (X[mask, left:right], X[mask_rest, left:right]) - ) + + def get_chunk(X, left, right): + return merge((X[mask, left:right], X[mask_rest, left:right])) else: n_cells = X.shape[0] - get_chunk = lambda X, left, right: adapt(X[:, left:right]) + + def get_chunk(X, left, right): + return adapt(X[:, left:right]) # Calculate chunk frames max_chunk = floor(CONST_MAX_SIZE / n_cells) @@ -170,10 +178,14 @@ def _basic_stats(self): del X_rest if issparse(self.X): - get_nonzeros = lambda X: X.getnnz(axis=0) + + def get_nonzeros(X): + return X.getnnz(axis=0) else: - get_nonzeros = lambda X: np.count_nonzero(X, axis=0) + + def get_nonzeros(X): + return np.count_nonzero(X, axis=0) for imask, mask in enumerate(self.groups_masks): X_mask = self.X[mask] @@ -743,7 +755,9 @@ def filter_rank_genes_groups( ) if 'log1p' in adata.uns_keys() and adata.uns['log1p']['base'] is not None: - expm1_func = lambda x: np.expm1(x * np.log(adata.uns['log1p']['base'])) + + def expm1_func(x): + return np.expm1(x * np.log(adata.uns['log1p']['base'])) else: expm1_func = np.expm1 From d89105f368fe13b36232d1b9a1531052d47df204 Mon Sep 17 00:00:00 2001 From: Zethson Date: Tue, 16 Mar 2021 14:16:14 +0100 Subject: [PATCH 65/85] import Literal Signed-off-by: Zethson --- scanpy/get/get.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scanpy/get/get.py b/scanpy/get/get.py index 10cde3f9e5..855292b260 100644 --- a/scanpy/get/get.py +++ b/scanpy/get/get.py @@ -6,7 +6,7 @@ from scipy.sparse import spmatrix from anndata import AnnData -import warnings +from ._compat import Literal # -------------------------------------------------------------------------------- # Plotting data helpers @@ -96,7 +96,7 @@ def rank_genes_groups_df( def _check_indices( dim_df: pd.DataFrame, alt_index: pd.Index, - dim: "Literal['obs', 'var']", # noqa: F821 + dim: "Literal['obs', 'var']", keys: List[str], alias_index: Optional[pd.Index] = None, use_raw: bool = False, From 5cdfa9d86b9286791884f128469434506e4a168c Mon Sep 17 00:00:00 2001 From: Zethson Date: Tue, 16 Mar 2021 14:23:25 +0100 Subject: [PATCH 66/85] revert literal import Signed-off-by: Zethson --- scanpy/get/get.py | 3 +-- scanpy/plotting/_tools/__init__.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/scanpy/get/get.py b/scanpy/get/get.py index 855292b260..88954c0d80 100644 --- a/scanpy/get/get.py +++ b/scanpy/get/get.py @@ -6,7 +6,6 @@ from scipy.sparse import spmatrix from anndata import AnnData -from ._compat import Literal # -------------------------------------------------------------------------------- # Plotting data helpers @@ -96,7 +95,7 @@ def rank_genes_groups_df( def _check_indices( dim_df: pd.DataFrame, alt_index: pd.Index, - dim: "Literal['obs', 'var']", + dim: "Literal['obs', 'var']", # noqa: F821 keys: List[str], alias_index: Optional[pd.Index] = None, use_raw: bool = False, diff --git a/scanpy/plotting/_tools/__init__.py b/scanpy/plotting/_tools/__init__.py index 1be61d2a0e..764d048ae6 100644 --- a/scanpy/plotting/_tools/__init__.py +++ b/scanpy/plotting/_tools/__init__.py @@ -365,7 +365,7 @@ def _fig_show_save_or_axes(plot_obj, return_fig, show, save): plot_obj.make_figure() savefig_or_show(plot_obj.DEFAULT_SAVE_PREFIX, show=show, save=save) show = settings.autoshow if show is None else show - if not show: + if show == False: # noqa: E712 return plot_obj.get_axes() From da412fcfdca796f4fcd47abab9e10ca3e5e13d5a Mon Sep 17 00:00:00 2001 From: Zethson Date: Tue, 16 Mar 2021 14:28:14 +0100 Subject: [PATCH 67/85] fix scatterplot pca import Signed-off-by: Zethson --- scanpy/plotting/_tools/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scanpy/plotting/_tools/__init__.py b/scanpy/plotting/_tools/__init__.py index 764d048ae6..6dc209e6b1 100644 --- a/scanpy/plotting/_tools/__init__.py +++ b/scanpy/plotting/_tools/__init__.py @@ -62,7 +62,7 @@ def pca_overview(adata: AnnData, **params): show = params['show'] if 'show' in params else None if 'show' in params: del params['show'] - scatterplots.pca(adata, **params, show=False) # noqa: F821 + pca(adata, **params, show=False) pca_loadings(adata, show=False) pca_variance_ratio(adata, show=show) From 220ac157bf47f9431671452bfc88f4466a72931f Mon Sep 17 00:00:00 2001 From: Zethson Date: Tue, 16 Mar 2021 14:33:44 +0100 Subject: [PATCH 68/85] false comparison & unused vars Signed-off-by: Zethson --- scanpy/plotting/_tools/paga.py | 2 +- scanpy/plotting/_utils.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/scanpy/plotting/_tools/paga.py b/scanpy/plotting/_tools/paga.py index 4f565b18ba..eaa25f4f73 100644 --- a/scanpy/plotting/_tools/paga.py +++ b/scanpy/plotting/_tools/paga.py @@ -148,7 +148,7 @@ def paga_compare( if suptitle is not None: pl.suptitle(suptitle) _utils.savefig_or_show('paga_compare', show=show, save=save) - if not show: + if show == False: # noqa: E712 return axs diff --git a/scanpy/plotting/_utils.py b/scanpy/plotting/_utils.py index 4ad2f861e6..68e8bfe25b 100644 --- a/scanpy/plotting/_utils.py +++ b/scanpy/plotting/_utils.py @@ -708,9 +708,7 @@ def scatter_base( ) for icolor, color in enumerate(colors): ax = axs[icolor] - left = panel_pos[2][2 * icolor] # noqa: F841 bottom = panel_pos[0][0] - width = draw_region_width / figure_width # noqa: F841 height = panel_pos[1][0] - bottom Y_sort = Y if not is_color_like(color) and sort_order: From f373a70b3d9233d7c971a6566f3ad23199223e7e Mon Sep 17 00:00:00 2001 From: Lukas Heumos Date: Tue, 16 Mar 2021 14:34:45 +0100 Subject: [PATCH 69/85] Add cleaner level determination Co-authored-by: Isaac Virshup --- scanpy/plotting/_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scanpy/plotting/_utils.py b/scanpy/plotting/_utils.py index 68e8bfe25b..da6c1abc79 100644 --- a/scanpy/plotting/_utils.py +++ b/scanpy/plotting/_utils.py @@ -938,8 +938,8 @@ def make_pos(pos, node=root, currentLevel=0, parent=None, vert_loc=0): if levels is None: levels = make_levels({}) else: - levels = {level: {TOTAL: levels[level], CURRENT: 0} for level in levels} - vert_gap = height / (max([level for level in levels]) + 1) + levels = {k: {TOTAL: v, CURRENT: 0} for k, v in levels.items()} + vert_gap = height / (max(levels.keys()) + 1) return make_pos({}) From 5adcfaebc707fa69474e2d4b37814f232d3d44f9 Mon Sep 17 00:00:00 2001 From: Lukas Heumos Date: Tue, 16 Mar 2021 14:35:38 +0100 Subject: [PATCH 70/85] Fix comment formatting Co-authored-by: Isaac Virshup --- scanpy/tools/_dpt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scanpy/tools/_dpt.py b/scanpy/tools/_dpt.py index 35937dc7f0..68d0fcaa2d 100644 --- a/scanpy/tools/_dpt.py +++ b/scanpy/tools/_dpt.py @@ -899,7 +899,7 @@ def _detect_branching_single_haghverdi16(self, Dseg, tips): # permutations of tip cells ps = [ [0, 1, 2], # start by computing distances from the first tip - [1, 2, 0], # -"- second tip + [1, 2, 0], # -"- second tip [2, 0, 1], ] # -"- third tip for i, p in enumerate(ps): From ce2fb44ad9cc6b5f951ca3bdcbe038ee3c50369d Mon Sep 17 00:00:00 2001 From: Lukas Heumos Date: Tue, 16 Mar 2021 14:36:17 +0100 Subject: [PATCH 71/85] Add smoother dev documentation Co-authored-by: Isaac Virshup --- docs/dev/code.rst | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/dev/code.rst b/docs/dev/code.rst index c1eaf86d37..4fc0a474f2 100644 --- a/docs/dev/code.rst +++ b/docs/dev/code.rst @@ -16,11 +16,10 @@ Code style ---------- New code should follow -`Black `__, +`Black `__ and `flake8 `__. -We ignore a couple of flake8 checks which are documented in the .flake8 file in the root -of this repository. +We ignore a couple of flake8 checks which are documented in the .flake8 file in the root of this repository. To learn how to ignore checks per line please read `flake8 violations `__. Additionally, we use Scanpy’s From 8d7e6e44212681450ed8375a637a33295af7de7c Mon Sep 17 00:00:00 2001 From: Zethson Date: Tue, 16 Mar 2021 14:38:37 +0100 Subject: [PATCH 72/85] fix flake8 Signed-off-by: Zethson --- .flake8 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.flake8 b/.flake8 index 4089ff0b50..5a438295e3 100644 --- a/.flake8 +++ b/.flake8 @@ -18,6 +18,8 @@ ignore = # module imported but unused -> required for Scanpys API # continuation line over-indented for hanging indent -> black does not adhere to PEP8 E126, # E266 too many leading '#' for block comment -> Scanpy allows them for comments into sections + E262 + # inline comment should start with '# ' -> Scanpy allows them for specific explanations E266 # Do not assign a lambda expression, use a def -> Scanpy allows lambda expression assignments, E731 From 64f6d7afcc1cbc3df5c62202e17b5df1da70d2e0 Mon Sep 17 00:00:00 2001 From: Lukas Heumos Date: Tue, 16 Mar 2021 14:43:22 +0100 Subject: [PATCH 73/85] Readd long comment Co-authored-by: Isaac Virshup --- scanpy/external/pl.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scanpy/external/pl.py b/scanpy/external/pl.py index 2c4167d72f..afa7b4acb3 100644 --- a/scanpy/external/pl.py +++ b/scanpy/external/pl.py @@ -334,7 +334,8 @@ def scrublet_score_distribution( """\ Plot histogram of doublet scores for observed transcriptomes and simulated doublets. - The histogram for simulated doublets is useful for determining the correct doublet score threshold. + The histogram for simulated doublets is useful for determining the correct doublet + score threshold. Parameters ---------- From 32dcf9677d93a7cfd823c02b2b853023a6b2418f Mon Sep 17 00:00:00 2001 From: Lukas Heumos Date: Tue, 16 Mar 2021 14:43:54 +0100 Subject: [PATCH 74/85] Assuming X as array like Co-authored-by: Isaac Virshup --- scanpy/preprocessing/_simple.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/scanpy/preprocessing/_simple.py b/scanpy/preprocessing/_simple.py index 344ea2022c..866e8e3183 100644 --- a/scanpy/preprocessing/_simple.py +++ b/scanpy/preprocessing/_simple.py @@ -752,9 +752,11 @@ def scale( annotated with `'mean'` and `'std'` in `adata.var`. """ _check_array_function_arguments(layer=layer, obsm=obsm) - return scale_array( - data, zero_center=zero_center, max_value=max_value, copy=copy # noqa: F821 - ) + if layer is not None: + raise ValueError(f"`layer` argument inappropriate for value of type {type(X)}") + if obsm is not None: + raise ValueError(f"`obsm` argument inappropriate for value of type {type(X)}") + return scale_array(X, zero_center=zero_center, max_value=max_value, copy=copy) @scale.register(np.ndarray) From 07cab3dd20026607478ef085e0310d1431bacea7 Mon Sep 17 00:00:00 2001 From: Zethson Date: Tue, 16 Mar 2021 14:44:38 +0100 Subject: [PATCH 75/85] fix flake8 Signed-off-by: Zethson --- scanpy/external/pl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scanpy/external/pl.py b/scanpy/external/pl.py index afa7b4acb3..3a59e99b61 100644 --- a/scanpy/external/pl.py +++ b/scanpy/external/pl.py @@ -335,7 +335,7 @@ def scrublet_score_distribution( Plot histogram of doublet scores for observed transcriptomes and simulated doublets. The histogram for simulated doublets is useful for determining the correct doublet - score threshold. + score threshold. Parameters ---------- From 699aaaccb53cee26324ea42df1d80e7bbc189110 Mon Sep 17 00:00:00 2001 From: Zethson Date: Tue, 16 Mar 2021 14:50:13 +0100 Subject: [PATCH 76/85] fix flake8 config Signed-off-by: Zethson --- .flake8 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.flake8 b/.flake8 index 5a438295e3..7f6b539fde 100644 --- a/.flake8 +++ b/.flake8 @@ -18,11 +18,11 @@ ignore = # module imported but unused -> required for Scanpys API # continuation line over-indented for hanging indent -> black does not adhere to PEP8 E126, # E266 too many leading '#' for block comment -> Scanpy allows them for comments into sections - E262 + E262, # inline comment should start with '# ' -> Scanpy allows them for specific explanations - E266 + E266, # Do not assign a lambda expression, use a def -> Scanpy allows lambda expression assignments, - E731 + E731, # allow I, O, l as variable names -> I is the identity matrix, i, j, k, l is reasonable indexing notation E741, exclude = From 79619ceef9936c06b9437cff3d4c97c81214ee3e Mon Sep 17 00:00:00 2001 From: Zethson Date: Tue, 16 Mar 2021 14:59:14 +0100 Subject: [PATCH 77/85] reverted rank_genes Signed-off-by: Zethson --- scanpy/tools/_dpt.py | 4 ++-- scanpy/tools/_rank_genes_groups.py | 38 ++++++++---------------------- 2 files changed, 12 insertions(+), 30 deletions(-) diff --git a/scanpy/tools/_dpt.py b/scanpy/tools/_dpt.py index 68d0fcaa2d..6521afecf3 100644 --- a/scanpy/tools/_dpt.py +++ b/scanpy/tools/_dpt.py @@ -899,9 +899,9 @@ def _detect_branching_single_haghverdi16(self, Dseg, tips): # permutations of tip cells ps = [ [0, 1, 2], # start by computing distances from the first tip - [1, 2, 0], # -"- second tip + [1, 2, 0], # -"- second tip [2, 0, 1], - ] # -"- third tip + ] # -"- third tip for i, p in enumerate(ps): ssegs.append(self.__detect_branching_haghverdi16(Dseg, tips[p])) return ssegs diff --git a/scanpy/tools/_rank_genes_groups.py b/scanpy/tools/_rank_genes_groups.py index 99393f691c..9f07c27eda 100644 --- a/scanpy/tools/_rank_genes_groups.py +++ b/scanpy/tools/_rank_genes_groups.py @@ -35,32 +35,22 @@ def _ranks(X, mask=None, mask_rest=None): n_genes = X.shape[1] if issparse(X): - - def merge(tpl): - return vstack(tpl).toarray() - - def adapt(X): - return X.toarray() - + merge = lambda tpl: vstack(tpl).toarray() + adapt = lambda X: X.toarray() else: merge = np.vstack - - def adapt(X): - return X + adapt = lambda X: X masked = mask is not None and mask_rest is not None if masked: n_cells = np.count_nonzero(mask) + np.count_nonzero(mask_rest) - - def get_chunk(X, left, right): - return merge((X[mask, left:right], X[mask_rest, left:right])) - + get_chunk = lambda X, left, right: merge( + (X[mask, left:right], X[mask_rest, left:right]) + ) else: n_cells = X.shape[0] - - def get_chunk(X, left, right): - return adapt(X[:, left:right]) + get_chunk = lambda X, left, right: adapt(X[:, left:right]) # Calculate chunk frames max_chunk = floor(CONST_MAX_SIZE / n_cells) @@ -178,14 +168,9 @@ def _basic_stats(self): del X_rest if issparse(self.X): - - def get_nonzeros(X): - return X.getnnz(axis=0) - + get_nonzeros = lambda X: X.getnnz(axis=0) else: - - def get_nonzeros(X): - return np.count_nonzero(X, axis=0) + get_nonzeros = lambda X: np.count_nonzero(X, axis=0) for imask, mask in enumerate(self.groups_masks): X_mask = self.X[mask] @@ -755,10 +740,7 @@ def filter_rank_genes_groups( ) if 'log1p' in adata.uns_keys() and adata.uns['log1p']['base'] is not None: - - def expm1_func(x): - return np.expm1(x * np.log(adata.uns['log1p']['base'])) - + expm1_func = lambda x: np.expm1(x * np.log(adata.uns['log1p']['base'])) else: expm1_func = np.expm1 From 99a8f2e0afd9ca6c77836dcd0d0fc11483834f0e Mon Sep 17 00:00:00 2001 From: Lukas Heumos Date: Tue, 16 Mar 2021 15:00:34 +0100 Subject: [PATCH 78/85] fix disp_mean_bin formatting Co-authored-by: Isaac Virshup --- scanpy/preprocessing/_deprecated/highly_variable_genes.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/scanpy/preprocessing/_deprecated/highly_variable_genes.py b/scanpy/preprocessing/_deprecated/highly_variable_genes.py index 9b4f834512..88454f08f8 100644 --- a/scanpy/preprocessing/_deprecated/highly_variable_genes.py +++ b/scanpy/preprocessing/_deprecated/highly_variable_genes.py @@ -170,10 +170,9 @@ def filter_genes_dispersion( disp_mean_bin[one_gene_per_bin] = 0 # actually do the normalization df['dispersion_norm'] = ( + # use values here as index differs df['dispersion'].values - - disp_mean_bin[ # use values here as index differs - df['mean_bin'].values - ].values + - disp_mean_bin[df['mean_bin'].values].values ) / disp_std_bin[df['mean_bin'].values].values elif flavor == 'cell_ranger': from statsmodels import robust From abe0846d307692c074f7f2b8757b18224414ec35 Mon Sep 17 00:00:00 2001 From: Zethson Date: Tue, 16 Mar 2021 15:07:11 +0100 Subject: [PATCH 79/85] fix formatting Signed-off-by: Zethson --- scanpy/tools/_dpt.py | 8 ++++++-- scanpy/tools/_sim.py | 10 ++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/scanpy/tools/_dpt.py b/scanpy/tools/_dpt.py index 6521afecf3..a02e8ace5f 100644 --- a/scanpy/tools/_dpt.py +++ b/scanpy/tools/_dpt.py @@ -342,7 +342,9 @@ def check_adjacency(self): for n_edges in range(1, np.max(n_edges_per_seg) + 1): for iseg in range(self.segs_adjacency.shape[0]): if n_edges_per_seg[iseg] == n_edges: - _ = self.segs_adjacency[iseg].todense().A1 + neighbor_segs = ( # noqa: F841 TODO Evaluate whether to assign the variable or not + self.segs_adjacency[iseg].todense().A1 + ) closest_points_other_segs = [ seg[np.argmin(self.distances_dpt[self.segs_tips[iseg][0], seg])] for seg in self.segs @@ -593,7 +595,9 @@ def detect_branching( for iseg, seg_connects in enumerate(ssegs_connects) if iseg != trunk ] - _ = segs_connects[iseg] + prev_connecting_points = segs_connects[ # noqa: F841 TODO Evaluate whether to assign the variable or not + iseg + ] for jseg_cnt, jseg in enumerate(prev_connecting_segments): iseg_cnt = 0 for iseg_new, seg_new in enumerate(ssegs): diff --git a/scanpy/tools/_sim.py b/scanpy/tools/_sim.py index 5bf79f8189..532f6db558 100644 --- a/scanpy/tools/_sim.py +++ b/scanpy/tools/_sim.py @@ -1074,6 +1074,16 @@ def sim_givenAdj(self, Adj: np.ndarray, model='line'): ------- Data array of shape (n_samples,dim). """ + # nice examples + examples = [ # noqa: F841 TODO We are really unsure whether this is needed. + dict( + func='sawtooth', + gdist='uniform', + sigma_glob=1.8, + sigma_noise=0.1, + ) + ] + # nr of samples n_samples = 100 From 16a0394b4dcbfc12e6bff7d82ef9b55a4b53c39d Mon Sep 17 00:00:00 2001 From: Zethson Date: Tue, 16 Mar 2021 15:19:42 +0100 Subject: [PATCH 80/85] add final todos Signed-off-by: Zethson --- .flake8 | 9 ++++---- scanpy/_utils.py | 4 +++- scanpy/get/get.py | 4 ++-- scanpy/plotting/_tools/__init__.py | 4 ++-- scanpy/plotting/_tools/paga.py | 2 +- scanpy/plotting/_utils.py | 4 +++- .../preprocessing/_highly_variable_genes.py | 6 ++--- scanpy/tests/test_pca.py | 4 ++-- scanpy/tools/_sim.py | 22 ++++++++++--------- 9 files changed, 32 insertions(+), 27 deletions(-) diff --git a/.flake8 b/.flake8 index 7f6b539fde..63a2344286 100644 --- a/.flake8 +++ b/.flake8 @@ -24,13 +24,14 @@ ignore = # module imported but unused -> required for Scanpys API # Do not assign a lambda expression, use a def -> Scanpy allows lambda expression assignments, E731, # allow I, O, l as variable names -> I is the identity matrix, i, j, k, l is reasonable indexing notation - E741, + E741 + per-file-ignores = + # F811 Redefinition of unused name from line, does not play nice with pytest fixtures + tests/test*.py: F811 exclude = .git, __pycache__, build, docs/_build, dist, -per-file-ignores = - # F811 Redefinition of unused name from line, does not play nice with pytest fixtures - tests/test*.py: F811, + diff --git a/scanpy/_utils.py b/scanpy/_utils.py index a2749c3e1b..101bab7712 100644 --- a/scanpy/_utils.py +++ b/scanpy/_utils.py @@ -554,7 +554,9 @@ def warn_with_traceback(message, category, filename, lineno, file=None, line=Non import traceback traceback.print_stack() - log = file if hasattr(file, 'write') else sys.stderr # noqa: F841 + log = ( # noqa: F841 # TODO Does this need fixing? + file if hasattr(file, 'write') else sys.stderr + ) settings.write(warnings.formatwarning(message, category, filename, lineno, line)) diff --git a/scanpy/get/get.py b/scanpy/get/get.py index 88954c0d80..dc79474db7 100644 --- a/scanpy/get/get.py +++ b/scanpy/get/get.py @@ -95,7 +95,7 @@ def rank_genes_groups_df( def _check_indices( dim_df: pd.DataFrame, alt_index: pd.Index, - dim: "Literal['obs', 'var']", # noqa: F821 + dim: "Literal['obs', 'var']", # noqa: F821 # TODO Does this need fixing? keys: List[str], alias_index: Optional[pd.Index] = None, use_raw: bool = False, @@ -175,7 +175,7 @@ def _get_array_values( X, dim_names: pd.Index, keys: List[str], - axis: "Literal[0, 1]", # noqa: F821 + axis: "Literal[0, 1]", # noqa: F821 # TODO Does this need fixing? backed: bool, ): # TODO: This should be made easier on the anndata side diff --git a/scanpy/plotting/_tools/__init__.py b/scanpy/plotting/_tools/__init__.py index 6dc209e6b1..7e9a886bb5 100644 --- a/scanpy/plotting/_tools/__init__.py +++ b/scanpy/plotting/_tools/__init__.py @@ -365,7 +365,7 @@ def _fig_show_save_or_axes(plot_obj, return_fig, show, save): plot_obj.make_figure() savefig_or_show(plot_obj.DEFAULT_SAVE_PREFIX, show=show, save=save) show = settings.autoshow if show is None else show - if show == False: # noqa: E712 + if show == False: # noqa: E712 # TODO Does this need fixing? -> is False return plot_obj.get_axes() @@ -967,7 +967,7 @@ def rank_genes_groups_violin( ) savefig_or_show(writekey, show=show, save=save) axs.append(_ax) - if not show: + if show == False: # noqa: E712 TODO Does this need fixing? return axs diff --git a/scanpy/plotting/_tools/paga.py b/scanpy/plotting/_tools/paga.py index eaa25f4f73..c1d19b94fc 100644 --- a/scanpy/plotting/_tools/paga.py +++ b/scanpy/plotting/_tools/paga.py @@ -148,7 +148,7 @@ def paga_compare( if suptitle is not None: pl.suptitle(suptitle) _utils.savefig_or_show('paga_compare', show=show, save=save) - if show == False: # noqa: E712 + if show == False: # noqa: E712 # TODO Does this need fixing? return axs diff --git a/scanpy/plotting/_utils.py b/scanpy/plotting/_utils.py index da6c1abc79..eaaa4ba32f 100644 --- a/scanpy/plotting/_utils.py +++ b/scanpy/plotting/_utils.py @@ -620,7 +620,9 @@ def setup_axes( figure_width = width_without_offsets + left_offset + right_offset draw_region_width_frac = draw_region_width / figure_width left_offset_frac = left_offset / figure_width - right_offset_frac = 1 - (len(panels) - 1) * left_offset_frac # noqa: F841 + right_offset_frac = ( # noqa: F841 # TODO Does this need fixing? + 1 - (len(panels) - 1) * left_offset_frac + ) if ax is None: pl.figure( diff --git a/scanpy/preprocessing/_highly_variable_genes.py b/scanpy/preprocessing/_highly_variable_genes.py index 796405e0c0..be2e0a1bd7 100644 --- a/scanpy/preprocessing/_highly_variable_genes.py +++ b/scanpy/preprocessing/_highly_variable_genes.py @@ -236,10 +236,8 @@ def _highly_variable_genes_single_batch( disp_mean_bin[one_gene_per_bin.values] = 0 # actually do the normalization df['dispersions_norm'] = ( - df['dispersions'].values - - disp_mean_bin[ - df['mean_bin'].values - ].values # use values here as index differs + df['dispersions'].values # use values here as index differs + - disp_mean_bin[df['mean_bin'].values].values ) / disp_std_bin[df['mean_bin'].values].values elif flavor == 'cell_ranger': from statsmodels import robust diff --git a/scanpy/tests/test_pca.py b/scanpy/tests/test_pca.py index 975f7ee34d..7d7837a9a0 100644 --- a/scanpy/tests/test_pca.py +++ b/scanpy/tests/test_pca.py @@ -38,7 +38,7 @@ ) -def test_pca_transform(array_type): # noqa: F811 +def test_pca_transform(array_type): A = array_type(A_list).astype('float32') A_pca_abs = np.abs(A_pca) A_svd_abs = np.abs(A_svd) @@ -101,7 +101,7 @@ def test_pca_sparse(pbmc3k_normalized): # This will take a while to run, but irreproducibility may # not show up for float32 unless the matrix is large enough -def test_pca_reproducible(pbmc3k_normalized, array_type, float_dtype): # noqa: F811 +def test_pca_reproducible(pbmc3k_normalized, array_type, float_dtype): pbmc = pbmc3k_normalized pbmc.X = array_type(pbmc.X) diff --git a/scanpy/tools/_sim.py b/scanpy/tools/_sim.py index 532f6db558..32f7099fa4 100644 --- a/scanpy/tools/_sim.py +++ b/scanpy/tools/_sim.py @@ -410,7 +410,7 @@ def __init__( if initType not in ['branch', 'random']: raise RuntimeError('initType must be either: branch, random') if model not in self.availModels.keys(): - message = 'model not among predefined models \n' # noqa: F841 + message = 'model not among predefined models \n' # noqa: F841 # TODO FIX # read from file from .. import sim_models @@ -771,11 +771,11 @@ def branch_init_model1(self, tmax=100): settings.m(0, '... initial point is too close to bounds') return None if self.show and self.verbosity > 1: - pl.figure() # noqa: F821 - pl.plot(XbackUp[:, 0], '.b', XbackUp[:, 1], '.g') # noqa: F821 - pl.plot(XbackDo[:, 0], '.b', XbackDo[:, 1], '.g') # noqa: F821 - pl.plot(Xup[:, 0], 'b', Xup[:, 1], 'g') # noqa: F821 - pl.plot(Xdo[:, 0], 'b', Xdo[:, 1], 'g') # noqa: F821 + pl.figure() # noqa: F821 TODO Fix me + pl.plot(XbackUp[:, 0], '.b', XbackUp[:, 1], '.g') # noqa: F821 TODO Fix me + pl.plot(XbackDo[:, 0], '.b', XbackDo[:, 1], '.g') # noqa: F821 TODO Fix me + pl.plot(Xup[:, 0], 'b', Xup[:, 1], 'g') # noqa: F821 TODO Fix me + pl.plot(Xdo[:, 0], 'b', Xdo[:, 1], 'g') # noqa: F821 TODO Fix me return X0mean def parents_from_boolRule(self, rule): @@ -1171,11 +1171,13 @@ def sim_combi(self): # AND type / horizontal X[:, 2] = func(X[:, 0]) * sp.stats.norm.cdf(X[:, 1], 1, 0.2) - pl.scatter(X[:, 0], X[:, 1], c=X[:, 2], edgecolor='face') # noqa: F821 - pl.show() # noqa: F821 + pl.scatter( # noqa: F821 TODO Fix me + X[:, 0], X[:, 1], c=X[:, 2], edgecolor='face' + ) + pl.show() # noqa: F821 TODO Fix me - pl.plot(X[:, 1], X[:, 2], '.') # noqa: F821 - pl.show() # noqa: F821 + pl.plot(X[:, 1], X[:, 2], '.') # noqa: F821 TODO Fix me + pl.show() # noqa: F821 TODO Fix me return X From 46f4ca7ee4da5919a418c6cdddfa97678c7c99e9 Mon Sep 17 00:00:00 2001 From: Zethson Date: Wed, 17 Mar 2021 10:01:04 +0100 Subject: [PATCH 81/85] boolean checks with is Signed-off-by: Zethson --- scanpy/plotting/_tools/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scanpy/plotting/_tools/__init__.py b/scanpy/plotting/_tools/__init__.py index 7e9a886bb5..8a63cfff0a 100644 --- a/scanpy/plotting/_tools/__init__.py +++ b/scanpy/plotting/_tools/__init__.py @@ -365,7 +365,7 @@ def _fig_show_save_or_axes(plot_obj, return_fig, show, save): plot_obj.make_figure() savefig_or_show(plot_obj.DEFAULT_SAVE_PREFIX, show=show, save=save) show = settings.autoshow if show is None else show - if show == False: # noqa: E712 # TODO Does this need fixing? -> is False + if show is False: return plot_obj.get_axes() @@ -967,7 +967,7 @@ def rank_genes_groups_violin( ) savefig_or_show(writekey, show=show, save=save) axs.append(_ax) - if show == False: # noqa: E712 TODO Does this need fixing? + if show is False: return axs From ad418d87212553a35593efed5d0015f2921e7d1a Mon Sep 17 00:00:00 2001 From: Zethson Date: Wed, 17 Mar 2021 10:02:56 +0100 Subject: [PATCH 82/85] _dpt formatting Signed-off-by: Zethson --- scanpy/plotting/_tools/paga.py | 2 +- scanpy/tools/_dpt.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/scanpy/plotting/_tools/paga.py b/scanpy/plotting/_tools/paga.py index c1d19b94fc..09bfb94712 100644 --- a/scanpy/plotting/_tools/paga.py +++ b/scanpy/plotting/_tools/paga.py @@ -148,7 +148,7 @@ def paga_compare( if suptitle is not None: pl.suptitle(suptitle) _utils.savefig_or_show('paga_compare', show=show, save=save) - if show == False: # noqa: E712 # TODO Does this need fixing? + if show is False: return axs diff --git a/scanpy/tools/_dpt.py b/scanpy/tools/_dpt.py index a02e8ace5f..e5a7221f4a 100644 --- a/scanpy/tools/_dpt.py +++ b/scanpy/tools/_dpt.py @@ -904,8 +904,8 @@ def _detect_branching_single_haghverdi16(self, Dseg, tips): ps = [ [0, 1, 2], # start by computing distances from the first tip [1, 2, 0], # -"- second tip - [2, 0, 1], - ] # -"- third tip + [2, 0, 1], # -"- third tip + ] for i, p in enumerate(ps): ssegs.append(self.__detect_branching_haghverdi16(Dseg, tips[p])) return ssegs From 10e5d76c1cf685890532cad8ee4b44843018f5dc Mon Sep 17 00:00:00 2001 From: Zethson Date: Wed, 17 Mar 2021 10:05:19 +0100 Subject: [PATCH 83/85] literal fixes Signed-off-by: Zethson --- scanpy/get/get.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scanpy/get/get.py b/scanpy/get/get.py index dc79474db7..a90b84b70c 100644 --- a/scanpy/get/get.py +++ b/scanpy/get/get.py @@ -6,6 +6,7 @@ from scipy.sparse import spmatrix from anndata import AnnData +from .._compat import Literal # -------------------------------------------------------------------------------- # Plotting data helpers @@ -95,7 +96,7 @@ def rank_genes_groups_df( def _check_indices( dim_df: pd.DataFrame, alt_index: pd.Index, - dim: "Literal['obs', 'var']", # noqa: F821 # TODO Does this need fixing? + dim: Literal['obs', 'var'], keys: List[str], alias_index: Optional[pd.Index] = None, use_raw: bool = False, @@ -175,7 +176,7 @@ def _get_array_values( X, dim_names: pd.Index, keys: List[str], - axis: "Literal[0, 1]", # noqa: F821 # TODO Does this need fixing? + axis: Literal[0, 1], backed: bool, ): # TODO: This should be made easier on the anndata side From 9b1da8cb2917afccd1ed3d71139efc60d460d43e Mon Sep 17 00:00:00 2001 From: Zethson Date: Wed, 17 Mar 2021 10:14:28 +0100 Subject: [PATCH 84/85] links to leafs Signed-off-by: Zethson --- scanpy/tools/_dpt.py | 8 ++++++-- scanpy/tools/_paga.py | 12 ++++++------ 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/scanpy/tools/_dpt.py b/scanpy/tools/_dpt.py index e5a7221f4a..716987f726 100644 --- a/scanpy/tools/_dpt.py +++ b/scanpy/tools/_dpt.py @@ -747,7 +747,11 @@ def _detect_branching( tips: np.ndarray, seg_reference=None, ) -> Tuple[ - List[np.ndarray], List[np.ndarray], List[List[int]], List[List[int]], int + List[np.ndarray], + List[np.ndarray], + List[List[int]], + List[List[int]], + int, ]: """\ Detect branching on given segment. @@ -984,7 +988,7 @@ def __detect_branching_haghverdi16( Dseg[tips[0]][idcs] + Dseg[tips[1]][idcs] + Dseg[tips[2]][idcs] ) # init list to store new segments - ssegs = [] # noqa: F841 + ssegs = [] # noqa: F841 # TODO Look into this # first new segment: all points until, but excluding the branching point # increasing the following slightly from imax is a more conservative choice # as the criterion based on normalized distances, which follows below, diff --git a/scanpy/tools/_paga.py b/scanpy/tools/_paga.py index d2000b23e9..a8d368badb 100644 --- a/scanpy/tools/_paga.py +++ b/scanpy/tools/_paga.py @@ -533,24 +533,24 @@ def paga_compare_paths( n_steps += 1 continue if len(path1) >= len(path2): - path_mapped = [asso_groups1[link] for link in path1] + path_mapped = [asso_groups1[leaf] for leaf in path1] path_compare = path2 path_compare_id = 2 path_compare_orig_names = [ - [orig_names2[int(s)] for s in link] for link in path_compare + [orig_names2[int(s)] for s in leaf] for leaf in path_compare ] path_mapped_orig_names = [ - [orig_names2[int(s)] for s in link] for link in path_mapped + [orig_names2[int(s)] for s in leaf] for leaf in path_mapped ] else: - path_mapped = [asso_groups2[link] for link in path2] + path_mapped = [asso_groups2[leaf] for leaf in path2] path_compare = path1 path_compare_id = 1 path_compare_orig_names = [ - [orig_names1[int(s)] for s in link] for link in path_compare + [orig_names1[int(s)] for s in leaf] for leaf in path_compare ] path_mapped_orig_names = [ - [orig_names1[int(s)] for s in link] for link in path_mapped + [orig_names1[int(s)] for s in leaf] for leaf in path_mapped ] n_agreeing_steps_path = 0 ip_progress = 0 From c372f0b25bb00a853d8ba20830251133f309bf7e Mon Sep 17 00:00:00 2001 From: Isaac Virshup Date: Thu, 18 Mar 2021 19:59:07 +1100 Subject: [PATCH 85/85] revert paga variable naming --- scanpy/tools/_paga.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/scanpy/tools/_paga.py b/scanpy/tools/_paga.py index a8d368badb..135c0ec6f9 100644 --- a/scanpy/tools/_paga.py +++ b/scanpy/tools/_paga.py @@ -533,24 +533,24 @@ def paga_compare_paths( n_steps += 1 continue if len(path1) >= len(path2): - path_mapped = [asso_groups1[leaf] for leaf in path1] + path_mapped = [asso_groups1[l] for l in path1] path_compare = path2 path_compare_id = 2 path_compare_orig_names = [ - [orig_names2[int(s)] for s in leaf] for leaf in path_compare + [orig_names2[int(s)] for s in l] for l in path_compare ] path_mapped_orig_names = [ - [orig_names2[int(s)] for s in leaf] for leaf in path_mapped + [orig_names2[int(s)] for s in l] for l in path_mapped ] else: - path_mapped = [asso_groups2[leaf] for leaf in path2] + path_mapped = [asso_groups2[l] for l in path2] path_compare = path1 path_compare_id = 1 path_compare_orig_names = [ - [orig_names1[int(s)] for s in leaf] for leaf in path_compare + [orig_names1[int(s)] for s in l] for l in path_compare ] path_mapped_orig_names = [ - [orig_names1[int(s)] for s in leaf] for leaf in path_mapped + [orig_names1[int(s)] for s in l] for l in path_mapped ] n_agreeing_steps_path = 0 ip_progress = 0