diff --git a/.gitignore b/.gitignore index 4bb9d22..fe47eb5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,50 +1,22 @@ -_build/ -data* -results* -logs* -_issues.ipynb -*/figures/* -figures/* -data/* -write/* - -*.egg-info/ -.pytest_cache/ -.coverage -cache/ -coverage.xml -test.h5ad -test_compr.h5ad -test_no_compr.h5ad -docs/_build -archive/ -data/ -README.html -scripts/ -test_notebooks.txt -upload_to_pypi.sh +/_build/ +/dist/ +*.h5ad +*.zip -# Ignore biomart file -.pybiomart.sqlite +/cache/ +/archive/ +/data/ +/write/ +/figures/ +/scripts/ -# always-ignore extensions +# caches *~ - -# Python / Byte-compiled / optimized / DLL __pycache__/ -*.py[cod] -*.so -.cache +.ipynb_checkpoints/ +.*cache/ # OS or Editor files and folders .DS_Store Thumbs.db -.ipynb_checkpoints/ .directory -/.idea/ -/.vscode/ -/hatch.toml - -# always-ignore directories -/dist/ -/build/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3e63860..de519a9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,15 +15,14 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.12.12 hooks: - - id: ruff - types_or: [python, pyi, jupyter] + - id: ruff-check args: ["--fix"] - id: ruff-format - types_or: [python, pyi, jupyter] - - repo: https://github.com/pre-commit/mirrors-prettier - rev: v4.0.0-alpha.8 + - repo: https://github.com/tox-dev/pyproject-fmt + rev: v2.6.0 hooks: - - id: prettier - exclude_types: - - markdown - language_version: 21.5.0 + - id: pyproject-fmt + - repo: https://github.com/biomejs/pre-commit + rev: v2.2.3 + hooks: + - id: biome-format diff --git a/.prettierrc.yaml b/.prettierrc.yaml deleted file mode 100644 index b10f6cc..0000000 --- a/.prettierrc.yaml +++ /dev/null @@ -1,7 +0,0 @@ -overrides: - # JSON with comments and trailing commas - - files: benchmarks/asv.conf.json - options: - parser: json5 - quoteProps: preserve - singleQuote: false diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..15b4d13 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,14 @@ +{ + // https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Build docs", + "type": "debugpy", + "request": "launch", + "module": "sphinx", + "args": ["-M", "html", ".", "_build"], + "console": "internalConsole", + }, + ], +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..a098bed --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,13 @@ +{ + "[python]": { + "editor.formatOnSave": true, + "editor.defaultFormatter": "charliermarsh.ruff", + "editor.codeActionsOnSave": { + "source.fixAll": "always", + "source.organizeImports": "always", + }, + }, + "notebook.formatOnSave.enabled": true, + "python.terminal.activateEnvironment": true, + "python.analysis.typeCheckingMode": "basic", +} diff --git a/_static/css/custom.css b/_static/css/custom.css index 03b7b0a..766febc 100644 --- a/_static/css/custom.css +++ b/_static/css/custom.css @@ -1,15 +1,15 @@ body { - background-image: repeating-linear-gradient( - 45deg, - var(--pst-color-danger-bg) 0, - var(--pst-color-danger-bg) 20px, - transparent 20px, - transparent 40px - ); + background-image: repeating-linear-gradient( + 45deg, + var(--pst-color-danger-bg) 0, + var(--pst-color-danger-bg) 20px, + transparent 20px, + transparent 40px + ); } .bd-sidebar-primary, .bd-sidebar-secondary, body:not(.scrolled) .bd-header-article { - background-color: initial !important; + background-color: initial !important; } diff --git a/basic-scrna-tutorial.ipynb b/basic-scrna-tutorial.ipynb index 19d3c49..d5ea25d 100644 --- a/basic-scrna-tutorial.ipynb +++ b/basic-scrna-tutorial.ipynb @@ -25,11 +25,13 @@ "outputs": [], "source": [ "# Core scverse libraries\n", - "import scanpy as sc\n", + "from __future__ import annotations\n", + "\n", "import anndata as ad\n", "\n", "# Data retrieval\n", - "import pooch" + "import pooch\n", + "import scanpy as sc" ] }, { @@ -185,9 +187,7 @@ "metadata": {}, "outputs": [], "source": [ - "sc.pp.calculate_qc_metrics(\n", - " adata, qc_vars=[\"mt\", \"ribo\", \"hb\"], inplace=True, log1p=True\n", - ")" + "sc.pp.calculate_qc_metrics(adata, qc_vars=[\"mt\", \"ribo\", \"hb\"], inplace=True, log1p=True)" ] }, { @@ -604,12 +604,13 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": null, "id": "11bb19be-3df1-4a17-ad84-16b50a9348ea", "metadata": {}, "outputs": [], "source": [ - "# Using the igraph implementation and a fixed number of iterations can be significantly faster, especially for larger datasets\n", + "# Using the igraph implementation and a fixed number of iterations can be significantly faster,\n", + "# especially for larger datasets\n", "sc.tl.leiden(adata, flavor=\"igraph\", n_iterations=2)" ] }, @@ -764,9 +765,7 @@ "outputs": [], "source": [ "for res in [0.02, 0.5, 2.0]:\n", - " sc.tl.leiden(\n", - " adata, key_added=f\"leiden_res_{res:4.2f}\", resolution=res, flavor=\"igraph\"\n", - " )" + " sc.tl.leiden(adata, key_added=f\"leiden_res_{res:4.2f}\", resolution=res, flavor=\"igraph\")" ] }, { @@ -1052,9 +1051,7 @@ } ], "source": [ - "sc.pl.rank_genes_groups_dotplot(\n", - " adata, groupby=\"leiden_res_0.50\", standard_scale=\"var\", n_genes=5\n", - ")" + "sc.pl.rank_genes_groups_dotplot(adata, groupby=\"leiden_res_0.50\", standard_scale=\"var\", n_genes=5)" ] }, { @@ -1214,7 +1211,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "notebook", "language": "python", "name": "python3" }, @@ -1228,7 +1225,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.8" + "version": "3.13.7" }, "mystnb": { "execution_mode": "off" diff --git a/biome.jsonc b/biome.jsonc new file mode 100644 index 0000000..60f4c36 --- /dev/null +++ b/biome.jsonc @@ -0,0 +1,18 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.1.1/schema.json", + "formatter": { "useEditorconfig": true }, + "overrides": [ + { + "includes": ["./.vscode/*.json", "**/*.jsonc", "**/asv.conf.json"], + "json": { + "formatter": { + "trailingCommas": "all", + }, + "parser": { + "allowComments": true, + "allowTrailingCommas": true, + }, + }, + }, + ], +} diff --git a/conf.py b/conf.py index 475f11a..ecd7d99 100644 --- a/conf.py +++ b/conf.py @@ -1,8 +1,9 @@ -from collections.abc import Mapping -from datetime import datetime +from __future__ import annotations + +from datetime import UTC, datetime from importlib.metadata import metadata from types import MappingProxyType -from typing import TYPE_CHECKING, Sequence +from typing import TYPE_CHECKING, ClassVar from docutils import nodes from sphinx import addnodes @@ -11,7 +12,10 @@ from sphinx.ext.intersphinx import resolve_reference_in_inventory from sphinx.util.docutils import SphinxDirective + if TYPE_CHECKING: + from collections.abc import Mapping, Sequence + from docutils.parsers.rst.states import Inliner from sphinx.application import Sphinx from sphinx.environment import BuildEnvironment @@ -20,7 +24,7 @@ meta = metadata("scanpy-tutorials") project = meta["Name"] author = meta["Author"] -copyright = f"{datetime.now():%Y}, {author}" +copyright = f"{datetime.now(UTC):%Y}, {author}" # noqa: A001 release = version = meta["Version"] extensions = [ @@ -48,7 +52,6 @@ anndata=("https://anndata.readthedocs.io/en/stable/", None), scanpy=("https://scanpy.readthedocs.io/en/stable/", None), ) -# TODO: move images here from scanpy suppress_warnings = ["image.not_readable"] # -- Options for HTML output ---------------------------------------------- @@ -88,6 +91,7 @@ def fake_cite( options: Mapping[str, object] = MappingProxyType({}), content: Sequence[str] = (), ) -> tuple[list[nodes.Node], list[str]]: + del name, lineno, options, content msg = f"cite:{text}" return [ inliner.document.reporter.info(msg), @@ -96,16 +100,15 @@ def fake_cite( class FakeDomain(Domain): - name = "cite" - roles = dict(p=fake_cite, t=fake_cite) + name: ClassVar = "cite" + roles: ClassVar = dict(p=fake_cite, t=fake_cite) # Role linking to the canonical location in scanpy’s docs MSG = ( - "Please access this document in its canonical location " - "as the currently accessed page may not be rendered correctly" + "Please access this document in its canonical location as the currently accessed page may not be rendered correctly" ) @@ -144,6 +147,7 @@ def missing_reference( node: addnodes.pending_xref, contnode: nodes.TextElement, ) -> nodes.Node | None: + del app, env, contnode # ignore known scanpy labels if node["reftarget"] in { "external-data-integration", diff --git a/dask.ipynb b/dask.ipynb index 50a862f..e83c669 100644 --- a/dask.ipynb +++ b/dask.ipynb @@ -63,12 +63,16 @@ } ], "source": [ + "from __future__ import annotations\n", + "\n", "from pathlib import Path\n", "\n", - "import dask.distributed as dd\n", - "import scanpy as sc\n", "import anndata as ad\n", + "import dask.distributed as dd\n", "import h5py\n", + "import pooch\n", + "import scanpy as sc\n", + "\n", "\n", "sc.logging.print_header()" ] @@ -80,45 +84,20 @@ "Here, we'll be working with a moderately large dataset of 1.4 million cells taken from: [COVID-19 immune features revealed by a large-scale single-cell transcriptome atlas](https://cellxgene.cziscience.com/collections/0a839c4b-10d0-4d64-9272-684c49a2c8ba)\n" ] }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "def download(url: str, path: Path) -> None:\n", - " \"\"\"Download a file from `url` and save it to `path`, showing a progress bar.\"\"\"\n", - " from tqdm.autonotebook import tqdm\n", - " from urllib.request import urlretrieve\n", - "\n", - " pb = tqdm(unit=\"B\", unit_scale=True, unit_divisor=1024)\n", - "\n", - " def update(b: int = 1, bsize: int = 1, tsize: int | None = None):\n", - " if tsize is not None:\n", - " pb.total = tsize\n", - " return pb.update(b * bsize - pb.n)\n", - "\n", - " try:\n", - " with pb:\n", - " urlretrieve(url, path, reporthook=update)\n", - " except BaseException:\n", - " path.unlink(missing_ok=True)\n", - " raise" - ] - }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ - "cell_atlas_path = Path(\"data/cell_atlas.h5ad\")\n", - "cell_atlas_path.parent.mkdir(exist_ok=True)\n", - "if not cell_atlas_path.exists():\n", - " download(\n", + "cell_atlas_path = Path(\n", + " pooch.retrieve(\n", " \"https://datasets.cellxgene.cziscience.com/82eac9c1-485f-4e21-ab21-8510823d4f6e.h5ad\",\n", - " cell_atlas_path,\n", - " )" + " known_hash=\"sha256:0b24babfb34b4af87a76806039afa3513c2c04c9045e2a9fb31a6e9350b1fabe\",\n", + " fname=\"cell_atlas.h5ad\",\n", + " path=\"data\",\n", + " )\n", + ")" ] }, { @@ -497,9 +476,7 @@ " obs=ad.io.read_elem(f[\"obs\"]),\n", " var=ad.io.read_elem(f[\"var\"]),\n", " )\n", - " adata.X = ad.experimental.read_elem_as_dask(\n", - " f[\"raw/X\"], chunks=(SPARSE_CHUNK_SIZE, adata.shape[1])\n", - " )" + " adata.X = ad.experimental.read_elem_as_dask(f[\"raw/X\"], chunks=(SPARSE_CHUNK_SIZE, adata.shape[1]))" ] }, { @@ -707,7 +684,8 @@ ], "source": [ "%%time\n", - "from sklearn_ann.kneighbors.annoy import AnnoyTransformer # noqa: E402\n", + "from sklearn_ann.kneighbors.annoy import AnnoyTransformer\n", + "\n", "\n", "transformer = AnnoyTransformer(n_neighbors=15)\n", "sc.pp.neighbors(adata, transformer=transformer)" @@ -772,7 +750,7 @@ ], "metadata": { "kernelspec": { - "display_name": "sc", + "display_name": "notebook", "language": "python", "name": "python3" }, @@ -786,7 +764,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.7" + "version": "3.13.7" } }, "nbformat": 4, diff --git a/how-to/knn-transformers.ipynb b/how-to/knn-transformers.ipynb index dd7371b..0e847db 100644 --- a/how-to/knn-transformers.ipynb +++ b/how-to/knn-transformers.ipynb @@ -39,6 +39,9 @@ } ], "source": [ + "from __future__ import annotations\n", + "\n", + "\n", "%pip install -qU \"pip\"\n", "%pip install -q \"scanpy\" \"sklearn-ann[annoy]\"" ] @@ -60,6 +63,7 @@ "import scanpy as sc\n", "from sklearn_ann.kneighbors.annoy import AnnoyTransformer # noqa: F401\n", "\n", + "\n", "sc.logging.print_header()" ] }, @@ -167,6 +171,7 @@ "source": [ "import matplotlib.pyplot as plt\n", "\n", + "\n", "with plt.ioff():\n", " fig, axs = plt.subplots(1, 3, figsize=(18, 5), layout=\"constrained\")\n", "sc.pl.umap(adata_default, color=\"leiden\", ax=axs[0], show=False, title=\"Default\")\n", diff --git a/how-to/plotting-with-marsilea.ipynb b/how-to/plotting-with-marsilea.ipynb index c39e3ab..684713f 100644 --- a/how-to/plotting-with-marsilea.ipynb +++ b/how-to/plotting-with-marsilea.ipynb @@ -52,9 +52,12 @@ } ], "source": [ + "from __future__ import annotations\n", + "\n", "import numpy as np\n", "import scanpy as sc\n", "\n", + "\n", "pbmc = sc.datasets.pbmc3k_processed().raw.to_adata()\n", "pbmc" ] @@ -69,7 +72,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "id": "bbc7e9ca-7e9a-4240-a8b1-bf438c2c82e4", "metadata": {}, "outputs": [], @@ -101,7 +104,7 @@ " \"#58DADA\",\n", " \"#F85959\",\n", "]\n", - "cmapper = dict(zip(uni_cells, cell_colors))" + "cmapper = dict(zip(uni_cells, cell_colors, strict=True))" ] }, { @@ -493,9 +496,7 @@ "m.group_cols(cells, order=uni_cells)\n", "\n", "m.add_right(mp.Labels(agg.obs[\"louvain\"], align=\"center\"), pad=0.1)\n", - "m.add_left(\n", - " mp.Numbers(agg_cell_counts, color=\"#EEB76B\", label=\"Count\"), size=0.5, pad=0.1\n", - ")\n", + "m.add_left(mp.Numbers(agg_cell_counts, color=\"#EEB76B\", label=\"Count\"), size=0.5, pad=0.1)\n", "m.add_dendrogram(\"right\", pad=0.1)\n", "m.add_legends()\n", "m.render()" @@ -511,7 +512,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": null, "id": "6f46dc1f-c2fe-4483-9a5d-d351798ef581", "metadata": {}, "outputs": [ @@ -532,7 +533,7 @@ "\n", "tp.group_cols(pbmc.obs[\"louvain\"], order=uni_cells, spacing=0.005)\n", "tp.add_dendrogram(\"top\", add_base=False, size=1)\n", - "for row, gene_name in zip(exp.T, markers):\n", + "for row in exp.T:\n", " area = mp.Area(\n", " row,\n", " add_outline=False,\n", @@ -556,7 +557,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": null, "id": "5b2f1772-b1fa-44dd-87cf-8921ba1782f4", "metadata": {}, "outputs": [ @@ -576,12 +577,13 @@ "from matplotlib.cm import ScalarMappable\n", "from matplotlib.colors import Normalize\n", "\n", + "\n", "gene_data = []\n", "cdata = []\n", - "for row, gene_name in zip(exp.T, markers[:5]):\n", + "for row, gene_name in zip(exp.T, markers[:5], strict=True):\n", " # Transform data to wide-format, marsilea only supports wide-format\n", " pdata = (\n", - " pd.DataFrame({\"exp\": row, \"cell_type\": pbmc.obs[\"louvain\"]})\n", + " pd.DataFrame({\"exp\": row, \"cell_type\": pbmc.obs[\"louvain\"]}) # noqa: PD010\n", " .reset_index(drop=True)\n", " .pivot(columns=\"cell_type\", values=\"exp\")\n", " )\n", @@ -788,6 +790,7 @@ "source": [ "import session_info\n", "\n", + "\n", "session_info.show(dependencies=True)" ] } diff --git a/integrating-data-using-ingest.ipynb b/integrating-data-using-ingest.ipynb index 52e303d..d631a84 100644 --- a/integrating-data-using-ingest.ipynb +++ b/integrating-data-using-ingest.ipynb @@ -42,9 +42,11 @@ "metadata": {}, "outputs": [], "source": [ + "from __future__ import annotations\n", + "\n", "import anndata\n", - "import scanpy as sc\n", - "import pandas as pd" + "import pandas as pd\n", + "import scanpy as sc" ] }, { @@ -327,9 +329,7 @@ "outputs": [], "source": [ "adata_concat.obs[\"louvain\"] = (\n", - " adata_concat.obs[\"louvain\"]\n", - " .astype(\"category\")\n", - " .cat.reorder_categories(adata_ref.obs[\"louvain\"].cat.categories)\n", + " adata_concat.obs[\"louvain\"].astype(\"category\").cat.reorder_categories(adata_ref.obs[\"louvain\"].cat.categories)\n", ")\n", "# fix category colors\n", "adata_concat.uns[\"louvain_colors\"] = adata_ref.uns[\"louvain_colors\"]" @@ -713,9 +713,7 @@ "# actually subset\n", "adata_all = adata_all[~adata_all.obs[\"celltype\"].isin(minority_classes)].copy()\n", "# reorder according to abundance\n", - "adata_all.obs[\"celltype\"] = adata_all.obs[\"celltype\"].cat.reorder_categories(\n", - " counts.index[:-5].tolist()\n", - ")" + "adata_all.obs[\"celltype\"] = adata_all.obs[\"celltype\"].cat.reorder_categories(counts.index[:-5].tolist())" ] }, { @@ -765,9 +763,7 @@ } ], "source": [ - "sc.pl.umap(\n", - " adata_all, color=[\"batch\", \"celltype\"], palette=sc.pl.palettes.vega_20_scanpy\n", - ")" + "sc.pl.umap(adata_all, color=[\"batch\", \"celltype\"], palette=sc.pl.palettes.vega_20_scanpy)" ] }, { @@ -1030,9 +1026,7 @@ "outputs": [], "source": [ "adata_concat.obs[\"celltype\"] = (\n", - " adata_concat.obs[\"celltype\"]\n", - " .astype(\"category\")\n", - " .cat.reorder_categories(adata_ref.obs[\"celltype\"].cat.categories)\n", + " adata_concat.obs[\"celltype\"].astype(\"category\").cat.reorder_categories(adata_ref.obs[\"celltype\"].cat.categories)\n", ")\n", "# fix category coloring\n", "adata_concat.uns[\"celltype_colors\"] = adata_ref.uns[\"celltype_colors\"]" @@ -1151,13 +1145,10 @@ ")\n", "# intersect categories\n", "obs_query_conserved = adata_query.obs.loc[\n", - " adata_query.obs[\"celltype\"].isin(conserved_categories)\n", - " & adata_query.obs[\"celltype_orig\"].isin(conserved_categories)\n", + " adata_query.obs[\"celltype\"].isin(conserved_categories) & adata_query.obs[\"celltype_orig\"].isin(conserved_categories)\n", "].copy()\n", "# remove unused categories\n", - "obs_query_conserved[\"celltype\"] = obs_query_conserved[\n", - " \"celltype\"\n", - "].cat.remove_unused_categories()\n", + "obs_query_conserved[\"celltype\"] = obs_query_conserved[\"celltype\"].cat.remove_unused_categories()\n", "# remove unused categories and fix category ordering\n", "obs_query_conserved[\"celltype_orig\"] = (\n", " obs_query_conserved[\"celltype_orig\"]\n", @@ -1720,7 +1711,7 @@ }, { "cell_type": "code", - "execution_count": 41, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -1742,8 +1733,9 @@ "source": [ "import matplotlib.pyplot as plt\n", "\n", + "\n", "fig, axes = plt.subplots(1, 3, figsize=(15, 5))\n", - "for batch, ax in zip([\"1\", \"2\", \"3\"], axes):\n", + "for batch, ax in zip([\"1\", \"2\", \"3\"], axes, strict=True):\n", " sc.pl.umap(adata_concat, color=\"batch\", groups=[batch], ax=ax, show=False)" ] } diff --git a/paga-paul15.ipynb b/paga-paul15.ipynb index 41ca519..0419191 100644 --- a/paga-paul15.ipynb +++ b/paga-paul15.ipynb @@ -44,12 +44,14 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ + "from __future__ import annotations\n", + "\n", + "import matplotlib.pyplot as plt\n", "import numpy as np\n", - "import matplotlib.pyplot as pl\n", "import scanpy as sc" ] }, @@ -751,9 +753,7 @@ } ], "source": [ - "sc.pl.draw_graph(\n", - " adata, color=[\"louvain_anno\", \"Itga2b\", \"Prss34\", \"Cma1\"], legend_loc=\"on data\"\n", - ")" + "sc.pl.draw_graph(adata, color=[\"louvain_anno\", \"Itga2b\", \"Prss34\", \"Cma1\"], legend_loc=\"on data\")" ] }, { @@ -778,9 +778,7 @@ } ], "source": [ - "sc.pl.draw_graph(\n", - " adata, color=[\"louvain_anno\", \"Itga2b\", \"Prss34\", \"Cma1\"], legend_loc=\"on data\"\n", - ")" + "sc.pl.draw_graph(adata, color=[\"louvain_anno\", \"Itga2b\", \"Prss34\", \"Cma1\"], legend_loc=\"on data\")" ] }, { @@ -792,7 +790,7 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -812,10 +810,10 @@ } ], "source": [ - "pl.figure(figsize=(8, 2))\n", + "plt.figure(figsize=(8, 2))\n", "for i in range(28):\n", - " pl.scatter(i, 1, c=sc.pl.palettes.zeileis_28[i], s=200)\n", - "pl.show()" + " plt.scatter(i, 1, c=sc.pl.palettes.zeileis_28[i], s=200)\n", + "plt.show()" ] }, { @@ -1074,7 +1072,7 @@ }, { "cell_type": "code", - "execution_count": 39, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -1104,10 +1102,8 @@ } ], "source": [ - "_, axs = pl.subplots(\n", - " ncols=3, figsize=(6, 2.5), gridspec_kw={\"wspace\": 0.05, \"left\": 0.12}\n", - ")\n", - "pl.subplots_adjust(left=0.05, right=0.98, top=0.82, bottom=0.2)\n", + "_, axs = plt.subplots(ncols=3, figsize=(6, 2.5), gridspec_kw={\"wspace\": 0.05, \"left\": 0.12})\n", + "plt.subplots_adjust(left=0.05, right=0.98, top=0.82, bottom=0.2)\n", "for ipath, (descr, path) in enumerate(paths):\n", " data = sc.pl.paga_path(\n", " adata,\n", @@ -1119,18 +1115,18 @@ " left_margin=0.15,\n", " n_avg=50,\n", " annotations=[\"distance\"],\n", - " show_yticks=True if ipath == 0 else False,\n", + " show_yticks=ipath == 0,\n", " show_colorbar=False,\n", " color_map=\"Greys\",\n", " groups_key=\"clusters\",\n", " color_maps_annotations={\"distance\": \"viridis\"},\n", - " title=\"{} path\".format(descr),\n", + " title=f\"{descr} path\",\n", " return_data=True,\n", " show=False,\n", " )\n", - " data.to_csv(\"./write/paga_path_{}.csv\".format(descr))\n", - "pl.savefig(\"./figures/paga_path_paul15.pdf\")\n", - "pl.show()" + " data.to_csv(f\"./write/paga_path_{descr}.csv\")\n", + "plt.savefig(\"./figures/paga_path_paul15.pdf\")\n", + "plt.show()" ] } ], diff --git a/pbmc3k.ipynb b/pbmc3k.ipynb old mode 100755 new mode 100644 index f277981..c07797a --- a/pbmc3k.ipynb +++ b/pbmc3k.ipynb @@ -21,9 +21,11 @@ "metadata": {}, "outputs": [], "source": [ + "from __future__ import annotations\n", + "\n", + "import matplotlib.pyplot as plt\n", "import pandas as pd\n", - "import scanpy as sc\n", - "import matplotlib.pyplot as plt" + "import scanpy as sc" ] }, { @@ -484,7 +486,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -500,7 +502,7 @@ } ], "source": [ - "# adata.X = adata.X.astype(\"int32\") # needed to match R results\n", + "# To match R results, do: `adata.X = adata.X.astype(\"int32\")`\n", "adata" ] }, @@ -615,9 +617,7 @@ "source": [ "# annotate the group of mitochondrial genes as \"mt\"\n", "adata.var[\"mt\"] = adata.var_names.str.startswith(\"MT-\")\n", - "sc.pp.calculate_qc_metrics(\n", - " adata, qc_vars=[\"mt\"], percent_top=None, log1p=False, inplace=True\n", - ")" + "sc.pp.calculate_qc_metrics(adata, qc_vars=[\"mt\"], percent_top=None, log1p=False, inplace=True)" ] }, { @@ -709,9 +709,7 @@ "outputs": [], "source": [ "adata = adata[\n", - " (adata.obs.n_genes_by_counts < 2500)\n", - " & (adata.obs.n_genes_by_counts > 200)\n", - " & (adata.obs.pct_counts_mt < 5),\n", + " (adata.obs.n_genes_by_counts < 2500) & (adata.obs.n_genes_by_counts > 200) & (adata.obs.pct_counts_mt < 5),\n", " :,\n", "].copy()\n", "adata.layers[\"counts\"] = adata.X.copy()" @@ -1419,9 +1417,7 @@ } ], "source": [ - "sc.tl.rank_genes_groups(\n", - " adata, \"leiden\", mask_var=\"highly_variable\", method=\"logreg\", max_iter=1000\n", - ")\n", + "sc.tl.rank_genes_groups(adata, \"leiden\", mask_var=\"highly_variable\", method=\"logreg\", max_iter=1000)\n", "sc.pl.rank_genes_groups(adata, n_genes=25, sharey=False)" ] }, @@ -2077,13 +2073,7 @@ "source": [ "result = adata.uns[\"rank_genes_groups\"]\n", "groups = result[\"names\"].dtype.names\n", - "pd.DataFrame(\n", - " {\n", - " f\"{group}_{key[:1]}\": result[key][group]\n", - " for group in groups\n", - " for key in [\"names\", \"pvals\"]\n", - " }\n", - ").head(5)" + "pd.DataFrame({f\"{group}_{key[:1]}\": result[key][group] for group in groups for key in [\"names\", \"pvals\"]}).head(5)" ] }, { @@ -2294,9 +2284,7 @@ } ], "source": [ - "sc.pl.umap(\n", - " adata, color=\"leiden\", legend_loc=\"on data\", title=\"\", frameon=False, save=\".pdf\"\n", - ")" + "sc.pl.umap(adata, color=\"leiden\", legend_loc=\"on data\", title=\"\", frameon=False, save=\".pdf\")" ] }, { @@ -2432,7 +2420,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.13.5" + "version": "3.13.7" } }, "nbformat": 4, diff --git a/plotting/advanced.ipynb b/plotting/advanced.ipynb index 8453dda..35b1653 100644 --- a/plotting/advanced.ipynb +++ b/plotting/advanced.ipynb @@ -21,22 +21,28 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "id": "641629fa", "metadata": {}, "outputs": [], "source": [ - "import scanpy as sc\n", - "import pandas as pd\n", - "import numpy as np\n", + "from __future__ import annotations\n", + "\n", + "from typing import TYPE_CHECKING\n", "\n", + "import matplotlib.colors as mcolors\n", "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import pandas as pd\n", + "import scanpy as sc\n", "import seaborn as sns\n", - "import matplotlib.colors as mcolors\n", "\n", "# Inital setting for plot size\n", "from matplotlib import rcParams\n", "\n", + "\n", + "rng = np.random.default_rng()\n", + "\n", "FIGSIZE = (3, 3)\n", "rcParams[\"figure.figsize\"] = FIGSIZE" ] @@ -217,9 +223,7 @@ "# finished plotting on all Axes and making all plot adjustments\n", "sc.pl.umap(adata, color=\"louvain\", ax=axs[0], show=False)\n", "# Example zoom-in into a subset of louvain clusters\n", - "sc.pl.umap(\n", - " adata[adata.obs.louvain.isin([\"0\", \"3\", \"9\"]), :], color=\"S_score\", ax=axs[1]\n", - ")" + "sc.pl.umap(adata[adata.obs.louvain.isin([\"0\", \"3\", \"9\"]), :], color=\"S_score\", ax=axs[1])" ] }, { @@ -438,9 +442,7 @@ "wspace = 1\n", "# Adapt figure size based on number of rows and columns and added space between them\n", "# (e.g. wspace between columns)\n", - "fig, axs = plt.subplots(\n", - " nrow, ncol, figsize=(ncol * figsize + (ncol - 1) * wspace * figsize, nrow * figsize)\n", - ")\n", + "fig, axs = plt.subplots(nrow, ncol, figsize=(ncol * figsize + (ncol - 1) * wspace * figsize, nrow * figsize))\n", "plt.subplots_adjust(wspace=wspace)\n", "sc.pl.umap(adata, color=\"louvain\", ax=axs[0], show=False)\n", "sc.pl.umap(adata, color=\"phase\", ax=axs[1])" @@ -665,7 +667,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": null, "id": "7112d43f", "metadata": {}, "outputs": [ @@ -685,12 +687,10 @@ "# To ensure that the ordering corresponds to cell cycle define order of categories;\n", "# this should include all categories in the corresponding pandas table column\n", "phases = [\"G1\", \"S\", \"G2M\"]\n", - "adata.obs[\"phase_ordered\"] = pd.Categorical(\n", - " values=adata.obs.phase, categories=phases, ordered=True\n", - ")\n", + "adata.obs[\"phase_ordered\"] = pd.Categorical(values=adata.obs.phase, categories=phases, ordered=True)\n", "sc.pl.umap(adata, color=[\"phase\", \"phase_ordered\"], wspace=0.5)\n", "# This just removes the newly added ordered column from adata as we do not need it below\n", - "adata.obs.drop(\"phase_ordered\", axis=1, inplace=True)" + "del adata.obs[\"phase_ordered\"]" ] }, { @@ -738,7 +738,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": null, "id": "7d9b11fb", "metadata": {}, "outputs": [ @@ -756,6 +756,7 @@ "source": [ "from matplotlib.lines import Line2D\n", "\n", + "\n", "fig = sc.pl.umap(adata, color=[\"bulk_labels\"], return_fig=True)\n", "ax = fig.axes[0]\n", "# Remove original Legend\n", @@ -776,9 +777,7 @@ " markersize=7,\n", " )\n", " # Color groups in adata\n", - " for l, c in zip(\n", - " list(adata.obs.bulk_labels.cat.categories), adata.uns[\"bulk_labels_colors\"]\n", - " )\n", + " for l, c in zip(list(adata.obs.bulk_labels.cat.categories), adata.uns[\"bulk_labels_colors\"], strict=True)\n", " ],\n", " # Customize Legend outline\n", " # Remove background\n", @@ -883,7 +882,7 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": null, "id": "de3b2707", "metadata": {}, "outputs": [ @@ -909,13 +908,12 @@ "# Package used for adding well aligned labels on the plot\n", "from adjustText import adjust_text\n", "\n", + "\n", "with plt.rc_context({\"figure.figsize\": (5, 5)}):\n", " x = \"means\"\n", " y = \"dispersions\"\n", " color = \"is_highly_variable\"\n", - " adata.var[\"is_highly_variable\"] = (\n", - " adata.var[\"highly_variable\"].astype(bool).astype(str)\n", - " )\n", + " adata.var[\"is_highly_variable\"] = adata.var[\"highly_variable\"].astype(bool).astype(str)\n", " ax = sc.pl.scatter(adata, x=x, y=y, color=color, show=False)\n", " print(\"Axes:\", ax)\n", " # Move plot title from Axes to Legend\n", @@ -940,8 +938,8 @@ " ]\n", " for gene in genes:\n", " # Position of object to be marked\n", - " x_loc = adata.var.at[gene, x]\n", - " y_loc = adata.var.at[gene, y]\n", + " x_loc = adata.var.loc[gene, x]\n", + " y_loc = adata.var.loc[gene, y]\n", " # Text color\n", " color_point = \"k\"\n", " texts.append(ax.text(x_loc, y_loc, gene, color=color_point, fontsize=10))\n", @@ -1042,7 +1040,7 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": null, "id": "7f79cb3d", "metadata": {}, "outputs": [ @@ -1062,16 +1060,16 @@ "\n", "# Make mock column for plotting, here we use random values from normal distribution\n", "loc = 0\n", - "adata.obs[\"normal\"] = np.random.normal(loc=loc, size=adata.shape[0])\n", + "adata.obs[\"normal\"] = rng.normal(loc=loc, size=adata.shape[0])\n", "\n", "# Center at mean (loc) of the distribution with vcenter parameter\n", "sc.pl.umap(adata, color=\"normal\", cmap=\"coolwarm\", s=20, vcenter=loc)\n", - "adata.obs.drop(\"normal\", axis=1, inplace=True)" + "del adata.obs[\"normal\"]" ] }, { "cell_type": "code", - "execution_count": 24, + "execution_count": null, "id": "7ba212d0", "metadata": {}, "outputs": [ @@ -1095,10 +1093,8 @@ "# To make a symmetric palette centerd around 0 we set vmax to maximal absolut value and vmin to\n", "# the negative value of maxabs\n", "maxabs = max(abs(adata.obs[\"B_cell_score\"]))\n", - "sc.pl.umap(\n", - " adata, color=\"B_cell_score\", cmap=\"coolwarm\", s=20, vmin=-maxabs, vmax=maxabs\n", - ")\n", - "adata.obs.drop(\"B_cell_score\", axis=1, inplace=True)" + "sc.pl.umap(adata, color=\"B_cell_score\", cmap=\"coolwarm\", s=20, vmin=-maxabs, vmax=maxabs)\n", + "del adata.obs[\"B_cell_score\"]" ] }, { @@ -1111,7 +1107,7 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": null, "id": "bdedcfc0", "metadata": {}, "outputs": [ @@ -1130,18 +1126,18 @@ "# Log-scaled palette\n", "\n", "# Make mock column with log-normally distirbuited values\n", - "adata.obs[\"lognormal\"] = np.random.lognormal(3, 1, adata.shape[0])\n", + "adata.obs[\"lognormal\"] = rng.lognormal(3, 1, adata.shape[0])\n", "\n", "# Log scaling of the palette\n", "norm = mcolors.LogNorm()\n", "sc.pl.umap(adata, color=\"lognormal\", s=20, norm=norm)\n", "\n", - "adata.obs.drop(\"lognormal\", axis=1, inplace=True)" + "del adata.obs[\"lognormal\"]" ] }, { "cell_type": "code", - "execution_count": 26, + "execution_count": null, "id": "31890a47", "metadata": {}, "outputs": [ @@ -1157,6 +1153,9 @@ } ], "source": [ + "if TYPE_CHECKING:\n", + " from numpy.typing import ArrayLike\n", + "\n", "# Centered non-symmetric palette\n", "\n", "# Make mock column for plotting, here we use B cell score\n", @@ -1167,21 +1166,26 @@ "# the distance of vmin and vmax from the cenetr\n", "# Adapted from https://stackoverflow.com/a/50003503\n", "class MidpointNormalize(mcolors.Normalize):\n", - " def __init__(self, vmin=None, vmax=None, midpoint=0, clip=False):\n", + " vmin: float\n", + " vmax: float\n", + " midpoint: float\n", + "\n", + " def __init__(\n", + " self, vmin: float | None = None, vmax: float | None = None, *, midpoint: float = 0, clip: bool = False\n", + " ) -> None:\n", " self.midpoint = midpoint\n", - " mcolors.Normalize.__init__(self, vmin, vmax, clip)\n", + " super().__init__(vmin, vmax, clip=clip)\n", "\n", - " def __call__(self, value, clip=None):\n", + " def __call__(self, value: ArrayLike, clip: object = None) -> np.ma.MaskedArray:\n", + " del clip\n", " value = np.array(value).astype(float)\n", " normalized_min = max(\n", " 0.0,\n", - " 0.5\n", - " * (1.0 - abs((self.midpoint - self.vmin) / (self.midpoint - self.vmax))),\n", + " 0.5 * (1.0 - abs((self.midpoint - self.vmin) / (self.midpoint - self.vmax))),\n", " )\n", " normalized_max = min(\n", " 1.0,\n", - " 0.5\n", - " * (1.0 + abs((self.vmax - self.midpoint) / (self.midpoint - self.vmin))),\n", + " 0.5 * (1.0 + abs((self.vmax - self.midpoint) / (self.midpoint - self.vmin))),\n", " )\n", " normalized_mid = 0.5\n", " x, y = (\n", @@ -1213,13 +1217,13 @@ ")\n", "# Adjust Colorbar ylim to be just outside of vmin,vmax and not far outside of this range\n", "# as the padding we set initially may be too broad\n", - "cmap_yticklabels = np.array([t._y for t in fig.axes[1].get_yticklabels()])\n", + "cmap_yticklabels = np.array([t.get_position()[1] for t in fig.axes[1].get_yticklabels()])\n", "fig.axes[1].set_ylim(\n", " max(cmap_yticklabels[cmap_yticklabels < vmin]),\n", " min(cmap_yticklabels[cmap_yticklabels > vmax]),\n", ")\n", "\n", - "adata.obs.drop(\"B_cell_score\", axis=1, inplace=True)" + "del adata.obs[\"B_cell_score\"]" ] }, { @@ -1334,7 +1338,7 @@ }, { "cell_type": "code", - "execution_count": 29, + "execution_count": null, "id": "d117c8cb", "metadata": {}, "outputs": [ @@ -1351,20 +1355,15 @@ ], "source": [ "# Make two batches in the adata object for the plot example\n", - "adata.obs[\"batch\"] = [\"a\"] * int(adata.shape[0] / 2) + [\"b\"] * (\n", - " adata.shape[0] - int(adata.shape[0] / 2)\n", - ")\n", + "adata.obs[\"batch\"] = [\"a\"] * int(adata.shape[0] / 2) + [\"b\"] * (adata.shape[0] - int(adata.shape[0] / 2))\n", "\n", "fig, axs = plt.subplots(1, 2, figsize=(9, 3))\n", "plt.subplots_adjust(wspace=1)\n", "sc.pl.umap(adata, color=\"batch\", ax=axs[0], title=\"Default ordering\", show=False)\n", "# Randomly order cells by making a random index and subsetting AnnData based on it\n", "# Set a random seed to ensure that the cell ordering will be reproducible\n", - "np.random.seed(0)\n", - "random_indices = np.random.permutation(list(range(adata.shape[0])))\n", - "sc.pl.umap(\n", - " adata[random_indices, :], color=\"batch\", ax=axs[1], title=\"Random re-ordering\"\n", - ")" + "random_indices = np.random.default_rng(0).permutation(list(range(adata.shape[0])))\n", + "sc.pl.umap(adata[random_indices, :], color=\"batch\", ax=axs[1], title=\"Random re-ordering\")" ] }, { @@ -1415,15 +1414,14 @@ "source": [ "from itertools import product\n", "\n", + "\n", "# Copy adata not to modify UMAP in the original adata object\n", "adata_temp = adata.copy()\n", "# Loop through different umap parameters, recomputting and replotting UMAP for each of them\n", "MIN_DISTS = [0.1, 1, 2]\n", "SPREADS = [0.5, 1, 5]\n", "# Create grid of plots, with a little extra room for the legends\n", - "fig, axes = plt.subplots(\n", - " len(MIN_DISTS), len(SPREADS), figsize=(len(SPREADS) * 3 + 2, len(MIN_DISTS) * 3)\n", - ")\n", + "fig, axes = plt.subplots(len(MIN_DISTS), len(SPREADS), figsize=(len(SPREADS) * 3 + 2, len(MIN_DISTS) * 3))\n", "\n", "for (i, min_dist), (j, spread) in product(enumerate(MIN_DISTS), enumerate(SPREADS)):\n", " ax = axes[i][j]\n", @@ -1565,9 +1563,7 @@ "source": [ "# Compare UMAP and PAGA layouts\n", "fig, axs = plt.subplots(1, 2, figsize=(6, 3))\n", - "sc.pl.umap(\n", - " adata, color=\"louvain\", ax=axs[0], show=False, title=\"UMAP\", legend_loc=\"on data\"\n", - ")\n", + "sc.pl.umap(adata, color=\"louvain\", ax=axs[0], show=False, title=\"UMAP\", legend_loc=\"on data\")\n", "sc.pl.paga(adata, ax=axs[1], title=\"PAGA\")" ] }, diff --git a/plotting/core.ipynb b/plotting/core.ipynb index e465b98..7854c40 100644 --- a/plotting/core.ipynb +++ b/plotting/core.ipynb @@ -70,6 +70,8 @@ "metadata": {}, "outputs": [], "source": [ + "from __future__ import annotations\n", + "\n", "import scanpy as sc\n", "from matplotlib.pyplot import rc_context" ] @@ -571,9 +573,7 @@ } ], "source": [ - "ax = sc.pl.stacked_violin(\n", - " pbmc, marker_genes_dict, groupby=\"clusters\", swap_axes=False, dendrogram=True\n", - ")" + "ax = sc.pl.stacked_violin(pbmc, marker_genes_dict, groupby=\"clusters\", swap_axes=False, dendrogram=True)" ] }, { @@ -715,17 +715,12 @@ "source": [ "import matplotlib.pyplot as plt\n", "\n", + "\n", "fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(20, 4), gridspec_kw={\"wspace\": 0.9})\n", "\n", - "ax1_dict = sc.pl.dotplot(\n", - " pbmc, marker_genes_dict, groupby=\"bulk_labels\", ax=ax1, show=False\n", - ")\n", - "ax2_dict = sc.pl.stacked_violin(\n", - " pbmc, marker_genes_dict, groupby=\"bulk_labels\", ax=ax2, show=False\n", - ")\n", - "ax3_dict = sc.pl.matrixplot(\n", - " pbmc, marker_genes_dict, groupby=\"bulk_labels\", ax=ax3, show=False, cmap=\"viridis\"\n", - ")" + "ax1_dict = sc.pl.dotplot(pbmc, marker_genes_dict, groupby=\"bulk_labels\", ax=ax1, show=False)\n", + "ax2_dict = sc.pl.stacked_violin(pbmc, marker_genes_dict, groupby=\"bulk_labels\", ax=ax2, show=False)\n", + "ax3_dict = sc.pl.matrixplot(pbmc, marker_genes_dict, groupby=\"bulk_labels\", ax=ax3, show=False, cmap=\"viridis\")" ] }, { @@ -764,9 +759,7 @@ } ], "source": [ - "ax = sc.pl.heatmap(\n", - " pbmc, marker_genes_dict, groupby=\"clusters\", cmap=\"viridis\", dendrogram=True\n", - ")" + "ax = sc.pl.heatmap(pbmc, marker_genes_dict, groupby=\"clusters\", cmap=\"viridis\", dendrogram=True)" ] }, { @@ -1038,9 +1031,7 @@ } ], "source": [ - "sc.pl.rank_genes_groups_matrixplot(\n", - " pbmc, n_genes=3, use_raw=False, vmin=-3, vmax=3, cmap=\"bwr\", layer=\"scaled\"\n", - ")" + "sc.pl.rank_genes_groups_matrixplot(pbmc, n_genes=3, use_raw=False, vmin=-3, vmax=3, cmap=\"bwr\", layer=\"scaled\")" ] }, { diff --git a/pyproject.toml b/pyproject.toml index e537954..2a378a1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,23 +1,33 @@ +[build-system] +build-backend = "hatchling.build" +requires = [ "hatch-vcs", "hatchling" ] + [project] name = "scanpy-tutorials" -dynamic = ["version"] authors = [ - { name = "Alex Wolf" }, - { name = "Fidel Ramirez" }, - { name = "Sergei Rybakov" }, + { name = "Alex Wolf" }, + { name = "Fidel Ramirez" }, + { name = "Sergei Rybakov" }, +] +requires-python = ">=3.11" +classifiers = [ + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", ] -requires-python = ">=3.9" +dynamic = [ "version" ] dependencies = [ - "sphinx", - "myst-nb", - "scanpydoc[theme]>=0.13.4", + "myst-nb", + "scanpydoc[theme]>=0.13.4", + "sphinx", ] [tool.hatch.version] source = "vcs" [tool.hatch.build.targets.wheel] -bypass-selection = true # This is not a package +bypass-selection = true # This is not a package [tool.hatch.envs.default] python = "3.13" @@ -29,19 +39,31 @@ clean = "git clean -fdX _build" [tool.hatch.envs.notebook] scripts.install-kernel = "python -m ipykernel install --user --name=scanpy-tutorials --display-name=\"Scanpy Tutorials\"" extra-dependencies = [ + "dask[distributed]", "ipykernel", "ipywidgets", - "scanpy[skmisc,leiden]", + "scanpy[skmisc,leiden,dask]", + "pooch", ] [tool.ruff] -extend-exclude = ["scanpy_workshop/"] -[tool.ruff.lint] -ignore = [ - "F821", # R cells can sometimes define variables - "E741", # Fonts these days don’t display I and l in a confusable way +line-length = 120 +extend-exclude = [ "scanpy_workshop/" ] +lint.select = [ "ALL" ] +lint.ignore = [ + "C408", # dict() calls are nice + "COM812", # trailing commas handled by black + "D", # no module docstrings for notebooks + "E741", # variable names aren’t really ambiguous + "F821", # `%%R -o` means that ruff doesn’t understand which variables exist + "FIX002", # “TODO” comments + "PLR0913", # having many (kw)args is fine + "PLR2004", # Magic numbers are sadly common + "Q", # handled by formatter + "S", # no need to worry about security here + "T201", # Allow print in notebooks ] - -[build-system] -build-backend = "hatchling.build" -requires = ["hatchling", "hatch-vcs"] +lint.allowed-confusables = [ "×", "’", "l" ] +lint.isort.known-first-party = [ "anndata2ri" ] +lint.isort.lines-after-imports = 2 +lint.isort.required-imports = [ "from __future__ import annotations" ] diff --git a/spatial/basic-analysis.ipynb b/spatial/basic-analysis.ipynb index 1da5603..12de302 100644 --- a/spatial/basic-analysis.ipynb +++ b/spatial/basic-analysis.ipynb @@ -37,9 +37,11 @@ "metadata": {}, "outputs": [], "source": [ - "import scanpy as sc\n", - "import pandas as pd\n", + "from __future__ import annotations\n", + "\n", "import matplotlib.pyplot as plt\n", + "import pandas as pd\n", + "import scanpy as sc\n", "import seaborn as sns" ] }, @@ -442,9 +444,7 @@ "sc.pp.pca(adata)\n", "sc.pp.neighbors(adata)\n", "sc.tl.umap(adata)\n", - "sc.tl.leiden(\n", - " adata, key_added=\"clusters\", flavor=\"igraph\", directed=False, n_iterations=2\n", - ")" + "sc.tl.leiden(adata, key_added=\"clusters\", flavor=\"igraph\", directed=False, n_iterations=2)" ] }, { diff --git a/spatial/integration-scanorama.ipynb b/spatial/integration-scanorama.ipynb index 720c3dd..8bf5242 100644 --- a/spatial/integration-scanorama.ipynb +++ b/spatial/integration-scanorama.ipynb @@ -59,15 +59,17 @@ "metadata": {}, "outputs": [], "source": [ - "import scanpy as sc\n", + "from __future__ import annotations\n", + "\n", + "from pathlib import Path\n", + "\n", "import anndata as an\n", - "import pandas as pd\n", - "import numpy as np\n", "import matplotlib.pyplot as plt\n", - "import seaborn as sns\n", + "import numpy as np\n", + "import pandas as pd\n", "import scanorama\n", - "\n", - "from pathlib import Path" + "import scanpy as sc\n", + "import seaborn as sns" ] }, { @@ -275,12 +277,8 @@ } ], "source": [ - "adata_spatial_anterior = sc.datasets.visium_sge(\n", - " sample_id=\"V1_Mouse_Brain_Sagittal_Anterior\"\n", - ")\n", - "adata_spatial_posterior = sc.datasets.visium_sge(\n", - " sample_id=\"V1_Mouse_Brain_Sagittal_Posterior\"\n", - ")" + "adata_spatial_anterior = sc.datasets.visium_sge(sample_id=\"V1_Mouse_Brain_Sagittal_Anterior\")\n", + "adata_spatial_posterior = sc.datasets.visium_sge(sample_id=\"V1_Mouse_Brain_Sagittal_Posterior\")" ] }, { @@ -491,9 +489,7 @@ "source": [ "sc.pp.neighbors(adata_spatial, use_rep=\"X_scanorama\")\n", "sc.tl.umap(adata_spatial)\n", - "sc.tl.leiden(\n", - " adata_spatial, key_added=\"clusters\", n_iterations=2, flavor=\"igraph\", directed=False\n", - ")" + "sc.tl.leiden(adata_spatial, key_added=\"clusters\", n_iterations=2, flavor=\"igraph\", directed=False)" ] }, { @@ -525,9 +521,7 @@ } ], "source": [ - "sc.pl.umap(\n", - " adata_spatial, color=[\"clusters\", \"library_id\"], palette=sc.pl.palettes.default_20\n", - ")" + "sc.pl.umap(adata_spatial, color=[\"clusters\", \"library_id\"], palette=sc.pl.palettes.default_20)" ] }, { @@ -539,13 +533,11 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "clusters_colors = dict(\n", - " zip([str(i) for i in range(18)], adata_spatial.uns[\"clusters_colors\"])\n", - ")" + "clusters_colors = dict(zip([str(i) for i in range(18)], adata_spatial.uns[\"clusters_colors\"], strict=True))" ] }, { @@ -580,9 +572,7 @@ "source": [ "fig, axs = plt.subplots(1, 2, figsize=(15, 10))\n", "\n", - "for i, library in enumerate(\n", - " [\"V1_Mouse_Brain_Sagittal_Anterior\", \"V1_Mouse_Brain_Sagittal_Posterior\"]\n", - "):\n", + "for i, library in enumerate([\"V1_Mouse_Brain_Sagittal_Anterior\", \"V1_Mouse_Brain_Sagittal_Posterior\"]):\n", " ad = adata_spatial[adata_spatial.obs.library_id == library, :].copy()\n", " sc.pl.spatial(\n", " ad,\n", @@ -590,11 +580,7 @@ " library_id=library,\n", " color=\"clusters\",\n", " size=1.5,\n", - " palette=[\n", - " v\n", - " for k, v in clusters_colors.items()\n", - " if k in ad.obs.clusters.unique().tolist()\n", - " ],\n", + " palette=[v for k, v in clusters_colors.items() if k in ad.obs.clusters.unique().tolist()],\n", " legend_loc=None,\n", " show=False,\n", " ax=axs[i],\n", @@ -632,50 +618,60 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "if not Path(\"./data/adata_processed.h5ad\").exists():\n", - " !wget https://ftp.ncbi.nlm.nih.gov/geo/series/GSE115nnn/GSE115746/suppl/GSE115746_cells_exon_counts.csv.gz -O data/GSE115746_cells_exon_counts.csv.gz\n", - " !gunzip data/GSE115746_cells_exon_counts.csv.gz\n", - " !wget https://ftp.ncbi.nlm.nih.gov/geo/series/GSE115nnn/GSE115746/suppl/GSE115746_complete_metadata_28706-cells.csv.gz -O data/GSE115746_complete_metadata_28706-cells.csv.gz\n", - " !gunzip data/GSE115746_complete_metadata_28706-cells.csv.gz\n", - " %pip install pybiomart\n", - " counts = pd.read_csv(\"data/GSE115746_cells_exon_counts.csv\", index_col=0).T\n", - " meta = pd.read_csv(\n", - " \"data/GSE115746_complete_metadata_28706-cells.csv\", index_col=\"sample_name\"\n", + "if not Path(\"cache/adata_processed.h5ad\").exists():\n", + " import pooch\n", + "\n", + " p_counts = Path(\n", + " pooch.retrieve(\n", + " \"https://ftp.ncbi.nlm.nih.gov/geo/series/GSE115nnn/GSE115746/suppl/GSE115746_cells_exon_counts.csv.gz\",\n", + " known_hash=\"sha256:5693f546dde28680d49bd7bf1255d42b0f77901aec050b94d56e54be10c00648\",\n", + " path=\"../data\",\n", + " )\n", " )\n", + " p_meta = Path(\n", + " pooch.retrieve(\n", + " \"https://ftp.ncbi.nlm.nih.gov/geo/series/GSE115nnn/GSE115746/suppl/GSE115746_complete_metadata_28706-cells.csv.gz\",\n", + " known_hash=\"sha256:381cc4dd26898016d506394b4cfbcebab38ac88d2f512ccf98216a5487db5bd2\",\n", + " path=\"../data\",\n", + " )\n", + " )\n", + "\n", + " counts = pd.read_csv(p_counts, compression=\"gzip\", index_col=0).T\n", + " meta = pd.read_csv(p_meta, compression=\"gzip\", index_col=\"sample_name\")\n", " meta = meta.loc[counts.index]\n", " annot = sc.queries.biomart_annotations(\n", " \"mmusculus\",\n", " [\"mgi_symbol\", \"ensembl_gene_id\"],\n", " ).set_index(\"mgi_symbol\")\n", " annot = annot[annot.index.isin(counts.columns)]\n", - " counts = counts.rename(columns=dict(zip(annot.index, annot[\"ensembl_gene_id\"])))\n", + " counts = counts.rename(columns=dict(zip(annot.index, annot[\"ensembl_gene_id\"], strict=True)))\n", " adata_cortex = an.AnnData(counts, obs=meta)\n", " sc.pp.normalize_total(adata_cortex, inplace=True)\n", " sc.pp.log1p(adata_cortex)\n", - " adata_cortex.write_h5ad(\"data/adata_processed.h5ad\")" + " adata_cortex.write_h5ad(\"cache/adata_processed.h5ad\")" ] }, { "cell_type": "code", - "execution_count": 14, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "adata_cortex = sc.read(\"./data/adata_processed.h5ad\")" + "adata_cortex = sc.read(\"cache/adata_processed.h5ad\")" ] }, { "cell_type": "code", - "execution_count": 15, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "adata_spatial_anterior.var.set_index(\"gene_ids\", inplace=True)\n", - "adata_spatial_posterior.var.set_index(\"gene_ids\", inplace=True)" + "adata_spatial_anterior.var_names = adata_spatial_anterior.var[\"gene_ids\"]\n", + "adata_spatial_posterior.var_names = adata_spatial_posterior.var[\"gene_ids\"]" ] }, { @@ -691,12 +687,9 @@ "metadata": {}, "outputs": [], "source": [ - "adata_anterior_subset = adata_spatial_anterior[\n", - " adata_spatial_anterior.obsm[\"spatial\"][:, 1] < 6000, :\n", - "]\n", + "adata_anterior_subset = adata_spatial_anterior[adata_spatial_anterior.obsm[\"spatial\"][:, 1] < 6000, :]\n", "adata_posterior_subset = adata_spatial_posterior[\n", - " (adata_spatial_posterior.obsm[\"spatial\"][:, 1] < 4000)\n", - " & (adata_spatial_posterior.obsm[\"spatial\"][:, 0] < 6000),\n", + " (adata_spatial_posterior.obsm[\"spatial\"][:, 1] < 4000) & (adata_spatial_posterior.obsm[\"spatial\"][:, 0] < 6000),\n", " :,\n", "]" ] @@ -793,21 +786,14 @@ "source": [ "from sklearn.metrics.pairwise import cosine_distances\n", "\n", + "\n", "distances_anterior = 1 - cosine_distances(\n", - " adata_cortex_anterior[adata_cortex_anterior.obs.dataset == \"smart-seq\"].obsm[\n", - " \"X_scanorama\"\n", - " ],\n", - " adata_cortex_anterior[adata_cortex_anterior.obs.dataset == \"visium\"].obsm[\n", - " \"X_scanorama\"\n", - " ],\n", + " adata_cortex_anterior[adata_cortex_anterior.obs.dataset == \"smart-seq\"].obsm[\"X_scanorama\"],\n", + " adata_cortex_anterior[adata_cortex_anterior.obs.dataset == \"visium\"].obsm[\"X_scanorama\"],\n", ")\n", "distances_posterior = 1 - cosine_distances(\n", - " adata_cortex_posterior[adata_cortex_posterior.obs.dataset == \"smart-seq\"].obsm[\n", - " \"X_scanorama\"\n", - " ],\n", - " adata_cortex_posterior[adata_cortex_posterior.obs.dataset == \"visium\"].obsm[\n", - " \"X_scanorama\"\n", - " ],\n", + " adata_cortex_posterior[adata_cortex_posterior.obs.dataset == \"smart-seq\"].obsm[\"X_scanorama\"],\n", + " adata_cortex_posterior[adata_cortex_posterior.obs.dataset == \"visium\"].obsm[\"X_scanorama\"],\n", ")" ] }, @@ -820,29 +806,26 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "def label_transfer(dist, labels):\n", + "def label_transfer(dist: np.ndarray, labels: pd.Series) -> np.ndarray:\n", " lab = pd.get_dummies(labels).to_numpy().T\n", " class_prob = lab @ dist\n", " norm = np.linalg.norm(class_prob, 2, axis=0)\n", " class_prob = class_prob / norm\n", - " class_prob = (class_prob.T - class_prob.min(1)) / class_prob.ptp(1)\n", - " return class_prob" + " return (class_prob.T - class_prob.min(1)) / class_prob.ptp(1)" ] }, { "cell_type": "code", - "execution_count": 21, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "class_prob_anterior = label_transfer(distances_anterior, adata_cortex.obs.cell_subclass)\n", - "class_prob_posterior = label_transfer(\n", - " distances_posterior, adata_cortex.obs.cell_subclass\n", - ")" + "class_prob_anterior = label_transfer(distances_anterior, adata_cortex.obs[\"cell_subclass\"])\n", + "class_prob_posterior = label_transfer(distances_posterior, adata_cortex.obs[\"cell_subclass\"])" ] }, { @@ -885,14 +868,10 @@ "outputs": [], "source": [ "adata_anterior_subset_transfer = adata_anterior_subset.copy()\n", - "adata_anterior_subset_transfer.obs = pd.concat(\n", - " [adata_anterior_subset.obs, cp_anterior_df], axis=1\n", - ")\n", + "adata_anterior_subset_transfer.obs = pd.concat([adata_anterior_subset.obs, cp_anterior_df], axis=1)\n", "\n", "adata_posterior_subset_transfer = adata_posterior_subset.copy()\n", - "adata_posterior_subset_transfer.obs = pd.concat(\n", - " [adata_posterior_subset.obs, cp_posterior_df], axis=1\n", - ")" + "adata_posterior_subset_transfer.obs = pd.concat([adata_posterior_subset.obs, cp_posterior_df], axis=1)" ] }, { @@ -999,12 +978,8 @@ } ], "source": [ - "sc.pl.spatial(\n", - " adata_anterior_subset_transfer, img_key=\"hires\", color=[\"Oligo\", \"Astro\"], size=1.5\n", - ")\n", - "sc.pl.spatial(\n", - " adata_posterior_subset_transfer, img_key=\"hires\", color=[\"Oligo\", \"Astro\"], size=1.5\n", - ")" + "sc.pl.spatial(adata_anterior_subset_transfer, img_key=\"hires\", color=[\"Oligo\", \"Astro\"], size=1.5)\n", + "sc.pl.spatial(adata_posterior_subset_transfer, img_key=\"hires\", color=[\"Oligo\", \"Astro\"], size=1.5)" ] }, { @@ -1026,7 +1001,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "notebook", "language": "python", "name": "python3" }, @@ -1040,7 +1015,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.6" + "version": "3.13.7" } }, "nbformat": 4, diff --git a/tutorial_pearson_residuals.ipynb b/tutorial_pearson_residuals.ipynb index 1094c3a..56420d1 100644 --- a/tutorial_pearson_residuals.ipynb +++ b/tutorial_pearson_residuals.ipynb @@ -76,10 +76,17 @@ } ], "source": [ - "import numpy as np\n", + "from __future__ import annotations\n", + "\n", + "import tarfile\n", + "from pathlib import Path\n", + "\n", "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import pooch\n", "import scanpy as sc\n", "\n", + "\n", "sc.settings.verbosity = 3 # verbosity: errors (0), warnings (1), info (2), hints (3)\n", "sc.logging.print_header()\n", "sc.settings.set_figure_params(dpi=80, facecolor=\"white\")" @@ -104,15 +111,25 @@ "metadata": {}, "outputs": [], "source": [ - "# !mkdir tutorial_data\n", - "# !mkdir tutorial_data/pbmc3k_v1\n", - "# !mkdir tutorial_data/pbmc10k_v3\n", - "\n", - "# !wget http://cf.10xgenomics.com/samples/cell-exp/1.1.0/pbmc3k/pbmc3k_filtered_gene_bc_matrices.tar.gz -O tutorial_data/pbmc3k_v1.tar.gz\n", - "# !cd tutorial_data; tar -xzf pbmc3k_v1.tar.gz -C pbmc3k_v1 --strip-components 2\n", + "p_pbmc3k = Path(\n", + " pooch.retrieve(\n", + " \"http://cf.10xgenomics.com/samples/cell-exp/1.1.0/pbmc3k/pbmc3k_filtered_gene_bc_matrices.tar.gz\",\n", + " known_hash=\"sha256:847d6ebd9a1ec9a768f2be7e40ca42cbfe75ebeb6d76a4c24167041699dc28b5\",\n", + " path=\"data/pbmc3k_v1\",\n", + " )\n", + ").relative_to(Path.cwd())\n", + "with tarfile.open(p_pbmc3k, \"r:gz\") as f:\n", + " f.extractall(p_pbmc3k.parent, filter=\"data\")\n", "\n", - "# !wget https://cf.10xgenomics.com/samples/cell-exp/3.0.0/pbmc_10k_v3/pbmc_10k_v3_filtered_feature_bc_matrix.tar.gz -O tutorial_data/pbmc10k_v3.tar.gz\n", - "# !cd tutorial_data; tar -xzf pbmc10k_v3.tar.gz -C pbmc10k_v3 --strip-components 1" + "p_pbmc10k = Path(\n", + " pooch.retrieve(\n", + " \"https://cf.10xgenomics.com/samples/cell-exp/3.0.0/pbmc_10k_v3/pbmc_10k_v3_filtered_feature_bc_matrix.tar.gz\",\n", + " known_hash=\"060af9e4370c6ad877e2987cf8cdc63a6d5c84e57875580f2be6c8e90748cf63\",\n", + " path=\"data/pbmc10k_v3\",\n", + " )\n", + ").relative_to(Path.cwd())\n", + "with tarfile.open(p_pbmc10k, \"r:gz\") as f:\n", + " f.extractall(p_pbmc10k.parent, filter=\"data\")" ] }, { @@ -138,8 +155,8 @@ } ], "source": [ - "adata_pbmc3k = sc.read_10x_mtx(\"tutorial_data/pbmc3k_v1/\", cache=True)\n", - "adata_pbmc10k = sc.read_10x_mtx(\"tutorial_data/pbmc10k_v3/\", cache=True)\n", + "adata_pbmc3k = sc.read_10x_mtx(p_pbmc3k.parent / \"filtered_gene_bc_matrices/hg19\", cache=True)\n", + "adata_pbmc10k = sc.read_10x_mtx(p_pbmc10k.parent / \"filtered_feature_bc_matrix\", cache=True)\n", "\n", "adata_pbmc3k.uns[\"name\"] = \"PBMC 3k (v1)\"\n", "adata_pbmc10k.uns[\"name\"] = \"PBMC 10k (v3)\"" @@ -244,9 +261,7 @@ "source": [ "for adata in [adata_pbmc3k, adata_pbmc10k]:\n", " adata.var[\"mt\"] = adata.var_names.str.startswith(\"MT-\")\n", - " sc.pp.calculate_qc_metrics(\n", - " adata, qc_vars=[\"mt\"], percent_top=None, log1p=False, inplace=True\n", - " )" + " sc.pp.calculate_qc_metrics(adata, qc_vars=[\"mt\"], percent_top=None, log1p=False, inplace=True)" ] }, { @@ -393,12 +408,9 @@ "adata_pbmc3k.obs[\"outlier_total\"] = adata_pbmc3k.obs.total_counts > 5000\n", "adata_pbmc3k.obs[\"outlier_ngenes\"] = adata_pbmc3k.obs.n_genes_by_counts > 2500\n", "\n", - "print(\n", - " \"%u cells with high %% of mitochondrial genes\"\n", - " % (sum(adata_pbmc3k.obs[\"outlier_mt\"]))\n", - ")\n", - "print(\"%u cells with large total counts\" % (sum(adata_pbmc3k.obs[\"outlier_total\"])))\n", - "print(\"%u cells with large number of genes\" % (sum(adata_pbmc3k.obs[\"outlier_ngenes\"])))\n", + "print(f\"{sum(adata_pbmc3k.obs['outlier_mt'])} cells with high % of mitochondrial genes\")\n", + "print(f\"{sum(adata_pbmc3k.obs['outlier_total'])} cells with large total counts\")\n", + "print(f\"{sum(adata_pbmc3k.obs['outlier_ngenes'])} cells with large number of genes\")\n", "\n", "adata_pbmc3k = adata_pbmc3k[~adata_pbmc3k.obs[\"outlier_mt\"], :]\n", "adata_pbmc3k = adata_pbmc3k[~adata_pbmc3k.obs[\"outlier_total\"], :]\n", @@ -436,14 +448,9 @@ "adata_pbmc10k.obs[\"outlier_total\"] = adata_pbmc10k.obs.total_counts > 25000\n", "adata_pbmc10k.obs[\"outlier_ngenes\"] = adata_pbmc10k.obs.n_genes_by_counts > 6000\n", "\n", - "print(\n", - " \"%u cells with high %% of mitochondrial genes\"\n", - " % (sum(adata_pbmc10k.obs[\"outlier_mt\"]))\n", - ")\n", - "print(\"%u cells with large total counts\" % (sum(adata_pbmc10k.obs[\"outlier_total\"])))\n", - "print(\n", - " \"%u cells with large number of genes\" % (sum(adata_pbmc10k.obs[\"outlier_ngenes\"]))\n", - ")\n", + "print(f\"{sum(adata_pbmc10k.obs['outlier_mt'])} cells with high % of mitochondrial genes\")\n", + "print(f\"{sum(adata_pbmc10k.obs['outlier_total'])} cells with large total counts\")\n", + "print(f\"{sum(adata_pbmc10k.obs['outlier_ngenes'])} cells with large number of genes\")\n", "\n", "adata_pbmc10k = adata_pbmc10k[~adata_pbmc10k.obs[\"outlier_mt\"], :]\n", "adata_pbmc10k = adata_pbmc10k[~adata_pbmc10k.obs[\"outlier_total\"], :]\n", @@ -514,9 +521,7 @@ ], "source": [ "for adata in [adata_pbmc3k, adata_pbmc10k]:\n", - " sc.experimental.pp.highly_variable_genes(\n", - " adata, flavor=\"pearson_residuals\", n_top_genes=2000\n", - " )" + " sc.experimental.pp.highly_variable_genes(adata, flavor=\"pearson_residuals\", n_top_genes=2000)" ] }, { @@ -535,7 +540,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -566,12 +571,10 @@ ], "source": [ "fig, axes = plt.subplots(1, 2, figsize=(12, 6))\n", - "for ax, adata in zip(axes, [adata_pbmc3k, adata_pbmc10k]):\n", + "for ax, adata in zip(axes, [adata_pbmc3k, adata_pbmc10k], strict=True):\n", " hvgs = adata.var[\"highly_variable\"]\n", "\n", - " ax.scatter(\n", - " adata.var[\"mean_counts\"], adata.var[\"residual_variances\"], s=3, edgecolor=\"none\"\n", - " )\n", + " ax.scatter(adata.var[\"mean_counts\"], adata.var[\"residual_variances\"], s=3, edgecolor=\"none\")\n", " ax.scatter(\n", " adata.var[\"mean_counts\"][hvgs],\n", " adata.var[\"residual_variances\"][hvgs],\n", @@ -740,14 +743,10 @@ "source": [ "# keep raw and depth-normalized counts for later\n", "adata_pbmc3k.layers[\"raw\"] = adata_pbmc3k.X.copy()\n", - "adata_pbmc3k.layers[\"sqrt_norm\"] = np.sqrt(\n", - " sc.pp.normalize_total(adata_pbmc3k, inplace=False)[\"X\"]\n", - ")\n", + "adata_pbmc3k.layers[\"sqrt_norm\"] = np.sqrt(sc.pp.normalize_total(adata_pbmc3k, inplace=False)[\"X\"])\n", "\n", "adata_pbmc10k.layers[\"raw\"] = adata_pbmc10k.X.copy()\n", - "adata_pbmc10k.layers[\"sqrt_norm\"] = np.sqrt(\n", - " sc.pp.normalize_total(adata_pbmc10k, inplace=False)[\"X\"]\n", - ")" + "adata_pbmc10k.layers[\"sqrt_norm\"] = np.sqrt(sc.pp.normalize_total(adata_pbmc10k, inplace=False)[\"X\"])" ] }, { @@ -1041,7 +1040,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "notebook", "language": "python", "name": "python3" }, @@ -1055,7 +1054,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.6" + "version": "3.13.7" } }, "nbformat": 4,