☰
Jupyter Notebook与Scanpy单细胞分析全流程:从环境配置到聚类实战
2026/10/20 6:10:54 网站建设 项目流程

Jupyter Notebook 是数据分析最常用的交互式编程环境,Scanpy 是目前单细胞转录组分析中使用最广的 Python 工具包之一。如果你刚进入单细胞数据分析这个方向,或者想把原来用 R 语言 Seurat 的流程迁移到 Python 生态,这两个是你绕不开的第一站。

这篇文章按我带新人时的顺序写:先装环境,再搞懂 Notebook 基础操作,然后用 Scanpy 跑一遍从质控到聚类的标准流程,最后整理几个新手最容易踩的坑。我的核心建议是:不要一上来就背 API,先把一条最小可运行的流程跑通,后面再往深了补会快很多。

1. 先搞清楚 Jupyter Notebook 和 Scanpy 各自解决什么问题

1.1 Jupyter Notebook 不是编辑器,而是一本可执行的分析笔记本

很多人第一次打开 Jupyter Notebook 会误以为它就是一个写 Python 的编辑器。实际上它的定位完全不同:它是一个交互式分析环境,核心单位是“单元格”(Cell),每个单元格可以放代码,也可以放 Markdown 说明文字,还能随时看到运行结果和图表。

数据分析工作和写网站不一样,它的特点是反复试探:先选一批数据,跑一步,看结果,再改参数,再跑一步。如果用普通.py脚本,每次改参数都要从头执行整个文件,中间结果只能靠 print 输出到控制台,图表还要另外弹出窗口。Jupyter Notebook 把这个过程变成了“在一个页面里边写、边跑、边记录、边调整”,哪一步结果有问题就单独改哪个单元格,不用重跑前面的代码。

这种交互方式非常适合 Scanpy 这类流程型工具。因为单细胞分析不是一次性写完就能出结果的,你需要随时观察数据分布、聚类效果和基因表达情况,然后决定下一步参数怎么调。

1.2 Scanpy 是把单细胞分析流程封装好的 Python 工具包

Scanpy 是一个基于 Python 的单细胞数据分析库,它的核心数据结构叫 AnnData。简单理解,AnnData 就是一张大表,行是细胞,列是基因,中间是表达量矩阵,同时还能保存细胞注释、基因注释、降维结果和聚类结果。

它解决了什么问题?传统做法里,从原始单细胞测序数据到“拿到一群有生物学意义的细胞类群”,中间要经历质控、归一化、高变基因筛选、降维、聚类、差异表达分析等多个步骤。如果每一步都自己写代码,光是数据格式转换就能劝退大部分人。Scanpy 把这些步骤封装成了一个个函数,例如sc.pp.filter_cells过滤细胞,sc.pp.normalize_total归一化,sc.tl.leiden做聚类。调用者只需要把 AnnData 对象按顺序传进去,结果就会自动更新到同一个对象里。

和 R 语言的 Seurat 相比,Scanpy 的优势在于它处在 Python 生态里。如果你后续要接深度学习模型、做批量处理、写自动化流程,或者和数据科学其他库配合,Python 顺手很多。入门阶段不需要纠结谁更好,先选一个能跑通的,把分析流程理解透,再看另一个会非常快。

1.3 为什么这两个东西经常一起出现

Jupyter Notebook 负责“交互”,Scanpy 负责“分析”,两者天然搭配。Scanpy 的绘图函数在 Notebook 里可以直接内嵌显示,不需要额外弹窗;每个单元格保持独立,跑完质控可以立即查看质控图,跑完聚类可以立即查看 UMAP 图上细胞分群效果。

如果你只是想在本地写一个一次性分析脚本,纯.py文件也能跑,但边跑边看、边看边改的体验远不如 Notebook。如果你是团队协作或者要复现结果,Notebook 又能把代码、输出和解释性文字放在一起,别人拿到文件就知道每一步在干什么、为什么这么干。这也是绝大多数单细胞分析教程都会用 Jupyter Notebook 呈现的原因。

2. 环境准备:Anaconda、Jupyter Notebook 与 JupyterLab 的取舍

2.1 先用 Anaconda 管理 Python 环境,少踩一半的坑

单细胞分析依赖的 Python 库比较多,而且很多库对版本敏感。Scanpy 依赖 numpy、pandas、scipy、scikit-learn、matplotlib、h5py 等一堆底层包,如果直接在系统 Python 里 pip install,很容易出现 A 库要求 numpy 1.x、B 库要求 numpy 2.x 这种问题。

所以我的建议是:先安装 Anaconda,用 conda 创建独立环境,不要装到 base 环境里。Anaconda 自带 conda、Python 和 Jupyter 相关组件,对 Windows、macOS、Linux 都支持,适合初学者。

创建环境的思路是这样:

conda create -n scanpy_env python=3.9

这条命令会创建一个名为scanpy_env的独立环境,里面装一个干净的 Python 3.9。之后安装任何包都在这个环境里操作,不会污染其他项目。

为什么不直接装在 base 里?因为 base 环境是 Anaconda 自带的,安装的包太多,版本冲突风险高。隔离环境之后,项目 A 用 Python 3.9,项目 B 用 Python 3.11,互不干扰,这在数据分析里非常重要。

2.2 Jupyter Notebook 和 JupyterLab 到底选哪个

很多新人会卡在这个问题上:搜索安装教程时,一会儿看到 Jupyter Notebook,一会儿看到 JupyterLab,到底装哪个?

实际上 JupyterLab 是 Jupyter 官方推荐的新一代交互界面,可以理解成 Notebook 的增强版。它支持多标签页、文件浏览器、终端、拖拽调整布局,功能更全。而传统 Jupyter Notebook 界面更简洁,很多老教程默认使用它。

对于 Scanpy 入门,两者都可以。我的建议是:装 JupyterLab,但了解两者的区别,因为你会遇到大量以 Notebook 名义写的旧教程。

对比项Jupyter NotebookJupyterLab
界面单文档为主,布局简单多标签页,支持文件管理器
单元格操作支持支持,功能更丰富
多文件同时打开较弱强,可并排查看
适合场景快速调试单个 ipynb长期项目、多文件协作
旧教程兼容高高

无论选哪个,它们的底层运行逻辑都一样:浏览器里打开的页面只是前端,真正执行代码的是后端内核(Kernel)。内核连接某个 Python 环境,你创建的scanpy_env如果想让 Notebook 识别,需要手动注册一下。

2.3 新建环境、安装 Scanpy 并让 Jupyter 识别内核

还是用命令行操作。先安装 JupyterLab,再安装 Scanpy:

conda activate scanpy_env conda install -c conda-forge jupyterlab pip install scanpy

说明一下为什么用conda install装 JupyterLab、用pip装 Scanpy。JupyterLab 从 conda-forge 源安装比较稳妥,会自动处理好依赖关系;Scanpy 用 pip 安装通常能拿到比较新的版本,安装速度也快。如果你发现某个包版本不对,再换成 conda 装也可以,不要死守一种方式。

安装完成后,在当前环境里运行:

python -m ipykernel install --user --name scanpy_env --display-name "Python (scanpy_env)"

这一步把当前环境注册为 Jupyter 的一个内核。之后无论你打开 Jupyter Notebook 还是 JupyterLab,新建 Notebook 时都能看到“Python (scanpy_env)”这个选项。选它,代码才会在装有 Scanpy 的环境里运行。

我见过很多新手在这一步出问题:明明已经pip install scanpy了,但在 Notebook 里import scanpy仍然报 ModuleNotFoundError。原因基本就是 Notebook 用的内核不是scanpy_env,而是另一个环境。所以注册内核这步不要跳过。

如果你用的是 PyCharm,也可以在 PyCharm 里直接创建或选择解释器,然后在编辑器里新建.ipynb文件,PyCharm 会调用选中的环境运行单元格。这种方式适合已经习惯 PyCharm 的人,但核心还是“内核对应环境”这个逻辑,搞懂这一点就不会乱。

3. Jupyter Notebook 基础操作:单元格、快捷键和魔法命令

3.1 两种最主要的单元格:代码和 Markdown

Jupyter Notebook 的页面由一个个单元格组成。最常见的两种类型是 Code 和 Markdown。

Code 单元格放 Python 代码,按Shift + Enter运行后会在单元格下方输出结果。Markdown 单元格放说明文字,支持标题、列表、公式等,用来记录每一步的分析逻辑。建议从一开始就养成习惯:每个分析段落上方加一个 Markdown 单元格,写清楚“这一段在做什么、为什么这么做”。几周后回来看这个 Notebook,你会感谢自己当时留了说明。

除了 Code 和 Markdown,还有 Raw 单元格,它只保存原始文本,不执行也不渲染,平时用得少,知道即可。

3.2 快捷键是效率分水岭

新人在 Notebook 里频繁用鼠标点击运行按钮,操作一多就很慢。其实 Notebook 有完整的键盘操作体系,核心是“命令模式”和“编辑模式”的切换。

按Esc进入命令模式,此时按字母键可以执行单元格操作;按Enter进入编辑模式,此时可以正常输入代码。命令模式下常用按键:

按键作用
A在当前单元格上方插入新单元格
B在当前单元格下方插入新单元格
DD删除当前单元格
M当前单元格转为 Markdown
Y当前单元格转为 Code
Shift + Enter运行当前单元格并进入下一个
Ctrl + Enter运行当前单元格但不跳转
Alt + Enter运行当前单元格并在下方新建一个

还有两个编辑模式下的快捷键特别实用:Tab补全变量名和函数名,Shift + Tab查看函数帮助。在 Notebook 里写 Scanpy 代码时,记不住参数名就多用Shift + Tab。

3.3 魔法命令和系统命令

Notebook 里有几个以%开头的“魔法命令”,对数据分析非常有用:

%timeit sum(range(10000))

%timeit会多次执行后面的语句并统计平均耗时,适合对比不同写法的性能。%%time写在单元格第一行,会统计整个单元格运行时间。%matplotlib inline用于让 matplotlib 图表直接内嵌显示,老教程里常见,新版本 JupyterLab 大多默认已支持,但仍建议写上,保证兼容性。

在代码单元格中用!开头可以直接执行系统命令,比如:

!ls

这样不用切到终端就能查看当前目录文件,配合%cd切换目录比较方便。不过要注意,!命令是在当前内核所在环境执行的,不是随便一个系统环境,遇到系统命令找不到时可以检查环境变量。

4. Scanpy 入门第一关:理解 AnnData 对象

4.1 AnnData 到底装了什么

Scanpy 几乎所有操作都围绕 AnnData 对象展开。可以把它理解成一个“自带说明书的表达矩阵”,主要包含这些部分:

属性内容举例
adata.X表达量矩阵,行是细胞,列是基因细胞数 x 基因数
adata.obs细胞层面的注释信息样本编号、批次、质控指标
adata.var基因层面的注释信息基因名、线粒体基因标记
adata.obsm降维后的细胞坐标PCA、UMAP
adata.uns其他分析结果和参数聚类信息、差异表达结果
adata.layers多层表达数据原始 counts 和归一化后的值同时保存

这个结构很有用。你不需要时刻记住哪个变量存了什么,因为 Scanpy 的每个函数会自动把结果写入对应位置。比如sc.tl.pca会把 PCA 结果写进adata.obsm['X_pca'],sc.tl.umap会写进adata.obsm['X_umap']。

一个常见错误是:在中间某步不小心重新赋值了adata,导致前面结果丢失。建议每次运行完一个阶段,先看一下adata的 shape 和 obs 列名,确认数据没有异常变化。

4.2 怎么把单细胞数据读进来

数据格式不同,读取函数也不同。最常见的是 10x Genomics 平台的输出,有两种情况:

import scanpy as sc # 情况一:h5 文件 adata = sc.read_10x_h5("filtered_feature_bc_matrix.h5") # 情况二:mtx 目录,包含 barcodes.tsv.gz 和 features.tsv.gz adata = sc.read_10x_mtx("data/filtered_gene_bc_matrices/hg19/") # 情况三:已经处理好的 h5ad 文件 adata = sc.read_h5ad("data.h5ad")

h5ad是 Scanpy 自己的保存格式,会把矩阵、注释、降维结果全部打包,复现和分享都很方便。分析完数据后可以用adata.write("save.h5ad")保存,下次直接读取,不用重跑。

如果是从 GEO 或其他数据库下载的矩阵,要先确认格式。有些是 csv 或 txt,可以先用 pandas 读进来,再手动构建 AnnData。这个扩展步骤比较灵活,关键是先看数据长什么样。

4.3 先用内置数据集把流程跑通

不用急着找自己的数据。Scanpy 内置了一些公开的小数据集合,最常用的是pbmc3k,来自 10x Genomics 平台的外周血单核细胞数据,约 2700 个细胞,几千个基因。这个数据体积小,跑起来很快,适合验证环境是否正常。

adata = sc.datasets.pbmc3k() print(adata)

如果这行代码能正常输出 AnnData 的摘要信息,说明 Scanpy 安装成功,数据结构也符合预期。后面所有流程都可以先用这个数据测试,等逻辑跑通了,再换自己的数据,避免一开始就被数据格式问题干扰。

5. 单细胞分析标准流程:从质控到聚类再找标记基因

5.1 质控过滤:先看数据分布,再定阈值

拿到表达矩阵后第一件事不是聚类,而是质控。原始数据里有很多质量差的细胞:基因检出数太少、总 UMI 数过低或过高、线粒体基因占比过高,这些都需要处理。

Scanpy 里先做基础过滤:

sc.pp.filter_cells(adata, min_genes=200) sc.pp.filter_genes(adata, min_cells=3)

第一行保留至少检测到 200 个基因的细胞,第二行保留至少在 3 个细胞中表达的基因。这样能去掉大部分空细胞和只在个别细胞里出现的基因。

接着计算质控指标:

adata.var["mt"] = adata.var_names.str.startswith("MT-") sc.pp.calculate_qc_metrics(adata, qc_vars=["mt"], percent_top=None, log1p=False, inplace=True)

MT-前缀对应线粒体基因。线粒体基因占比过高通常表示细胞状态不好,常见阈值为 20% 或 10%。但我不建议直接照搬数字,最好先用sc.pl.violin画一下分布图,再根据数据实际情况决定阈值。

过滤的时候有两种写法:一种是直接原地过滤,另一种是生成一个新的 AnnData,保留原对象。新手容易忽略的是,多次原地过滤之后原始信息丢失,后面想回头查某个细胞为什么被删掉就麻烦了。所以建议在obs里先加一列标记,例如adata.obs["pass_qc"],再根据标记筛选。

5.2 归一化和高变基因筛选

质控之后,不同细胞的测序深度差异很大,直接比较表达量没有意义。Scanpy 的标准做法是先归一化,取对数,再筛选高变基因:

sc.pp.normalize_total(adata, target_sum=1e4) sc.pp.log1p(adata)

normalize_total把每个细胞的总表达量标准化到同一个量级,log1p取对数,把数据分布变得更接近正态,减少极端值的影响。这两步几乎是必做的,不要跳过。

接下来选高变基因:

sc.pp.highly_variable_genes(adata, min_mean=0.0125, max_mean=3, min_disp=0.5) adata.raw = adata adata = adata[:, adata.var.highly_variable]

高变基因是不同细胞间表达差异较大的基因,用它做后续聚类,能减少噪声,突出细胞类型差异。min_mean、max_mean和min_disp是控制筛选范围的参数,不同数据可能要微调,但入门阶段用默认值即可。

adata.raw = adata这行很关键:它把当前归一化后的数据保存为原始参考,之后如果你对数据做了切片、缩放,后续找标记基因时仍然可以对照完整数据。很多人因为忘记保存raw,导致rank_genes_groups报错或者结果意义不明。

最后一步是标准化:

sc.pp.scale(adata, max_value=10)

这一步让每个基因的表达量均值为 0、方差为 1,避免高表达基因主导 PCA。max_value=10限制最大值,防止极端离群点影响计算。

5.3 PCA、邻居图、UMAP 和聚类

高变基因筛完后,数据维度还是很高,直接用原始维度算距离会非常慢。Scanpy 的流程是先做 PCA 降维,再用 PCA 结果构建细胞间邻居图,最后用 UMAP 可视化、用 Leiden 聚类。

sc.tl.pca(adata, svd_solver="arpack") sc.pl.pca_variance_ratio(adata, n_pcs=50)

pca_variance_ratio会画出一张图,展示前 50 个主成分各自解释了多少方差。一般选累计方差占比开始变平前的 PC 数量作为后续参数,不必纠结太多,20 到 30 是常见选择。

接下来:

sc.pp.neighbors(adata, n_neighbors=10, n_pcs=30) sc.tl.umap(adata) sc.pl.umap(adata, color="leiden")

这里涉及两个重要参数。n_neighbors控制邻居数量,调大后 UMAP 图更平滑但会丢失局部结构,调小则更散但噪声多。n_pcs是使用多少个主成分构建邻居图,会和刚才 PCA 结果对应起来。新手最容易犯的错是上来就把n_neighbors调得很大,结果图是漂亮了,但聚类结果没有生物学意义。

聚类用 Leiden:

sc.tl.leiden(adata, resolution=0.5)

resolution是聚类分辨率,数值越大分出的类越多。比如0.1可能分成 3 群,1.0可能分成 10 群。具体调到多少,关键看后续标记基因能否说明每个群的生物学身份。

5.4 标记基因与可视化输出

聚类完成后,每个群是数字编号,需要确定它属于什么细胞类型。常用方法是差异表达分析,找到每个群相对其他群高表达的基因:

sc.tl.rank_genes_groups(adata, "leiden", method="wilcoxon") sc.pl.rank_genes_groups(adata, n_genes=20, sharey=False)

method参数有wilcoxon、t-test、logreg等,入门阶段用wilcoxon比较稳妥,它对数据分布假设更少。运行后会得到一个气泡图或排行榜,展示每个群的 top 基因。

拿到标记基因后,通常需要人工对照已知细胞类型标记物。比如 T 细胞的 CD3D、CD3E,B 细胞的 MS4A1、CD79A,单核细胞的 LYZ、CD14 等。Scanpy 也支持直接可视化某个基因在各群中的表达:

sc.pl.umap(adata, color=["CD3D", "MS4A1", "LYZ"])

这一步是分析里最费时间、也最需要领域知识的部分。机器只能告诉你分了几群,群是什么细胞类型,必须靠你对照标记基因、结合背景知识判断。这也是单细胞分析不能全自动跑完的原因。

6. 实际运行中常见的坑和排查顺序

6.1 环境类报错:先看内核,再看依赖

新手第一个报错大概率是ModuleNotFoundError: No module named 'scanpy'。遇到这个报错,先不要急着重新安装,而是确认当前 Notebook 用的是哪个内核。可以新开一个单元格运行:

import sys print(sys.executable)

如果输出路径不是你创建scanpy_env对应的路径,说明内核选错了。在 Notebook 界面右上角切换内核,或者在 JupyterLab 里重新选择,再运行一次。

还有一种情况是ipykernel没装或者版本不对,导致启动内核失败。可以回到终端重新安装:

pip install --upgrade ipykernel python -m ipykernel install --user --name scanpy_env --display-name "Python (scanpy_env)"

如果报错信息是ImportError且涉及 numpy 或 numba,大概率是依赖版本冲突。Scanpy 对 numpy 版本有要求,试一下:

pip install --upgrade scanpy

或者用 conda 重新解析依赖:

conda install -c conda-forge scanpy python=3.9

排错时记住一个顺序:先看报错类型,再确认环境,最后再看代码逻辑。不要看到一条堆栈信息就以为是模型或数据的问题,很多时候第一个异常前面的提示才是真正原因。

6.2 页面空白、端口占用和浏览器问题

Windows 下打开 Jupyter Notebook 出现空白页,是热搜里经常出现的问题。这类问题通常不在 Python 代码里,而在启动方式或浏览器环境上。

排查步骤我一般这样走:

  1. 确认启动命令是在终端执行jupyter notebook或jupyter lab,不是双击某个快捷方式后立刻关闭。
  2. 看启动终端里是否显示http://localhost:8888。如果端口被占用,会变成8889、8890,此时打开旧地址当然空白。
  3. 换浏览器。Chrome 和 Firefox 兼容性相对好,某些内嵌浏览器或旧版本浏览器可能渲染异常。
  4. 清除浏览器缓存,强制刷新一次页面。
  5. 如果配置了本机防火墙,确认允许 Python 进程监听本地端口。

还有一个容易被忽略的点:启动 Jupyter 的终端目录是什么,Notebook 的根目录就是什么。如果你在某个深层目录里启动,然后用浏览器在文件树里找不到之前的文件,很常见。统一在项目根目录启动,文件管理会清楚很多。

6.3 内存不足和运行过慢

单细胞数据一大,内存问题就来了。几万个细胞、两三万个基因的表达矩阵,在内存里其实非常大。你可能会遇到内核无预警退出,或者运行 PCA 时电脑直接卡死。

先看几个缓解办法:

  • 质控阶段尽早过滤,把无效细胞和基因删掉,矩阵规模越小越省内存。
  • 使用sc.read_10x_h5时保留原始数据,但分析过程中尽量用float32类型,Scanpy 默认会处理。
  • 不要一次把多个样本读进同一个对象再慢慢处理,先逐个做基础质控,再合并。
  • 用adata.write保存中间结果的 h5ad 文件,分阶段运行,避免一个 Notebook 长时间占用内存。

如果数据实在太大,考虑用sc.pp.subsample先随机抽样一部分细胞验证流程,全量分析放到更高配置的机器上。这不是偷懒,而是很实用的调试方式。

6.4 结果不对时优先检查数据和参数

聚类结果一团糟时,先别怀疑算法有问题,优先检查这几个地方:读入的数据是否经过归一化、高变基因筛选是否生效、adata.raw是否保存、PCA 是否有足够的主成分解释方差。一个常见错误是先做scale再做highly_variable_genes,顺序反了,筛选结果会异常。Scanpy 官方推荐顺序是 normalize、log1p、highly_variable_genes、scale,按这个顺序跑能减少很多问题。

另一个观察点是 UMAP 图是否过度拥挤或完全分离。如果图完全分成一个个小点,像噪声一样,可能是n_neighbors太小;如果所有细胞混成一大团,可能是批次效应没有去除,或者聚类分辨率偏低。每一步都结合分布图和统计指标判断,不要只看最后的 UMAP 好不好看。

7. 给新手的几个务实建议

7.1 先跑通最小流程,再考虑细节优化

学习 Scanpy 最容易陷入的误区是:还没跑通流程,就开始纠结某个参数的精确定义,或者想把所有高级图都画出来。更好的做法是先用pbmc3k把标准流程完整跑一遍,哪怕结果是默认参数出来的,先形成一个完整的分析闭环,再回头逐段优化。

我的经验是,第一次完整跑通之后,你对 AnnData、函数之间的协作方式和输出结果的组织结构会有整体认知。之后再换自己的数据,每一步都知道“现在在做什么、下一步会得到什么”,学习效率会高很多。

7.2 把版本和关键参数记录到 Notebook 里

单细胞分析的可复现性非常依赖环境和参数。建议在 Notebook 的第一个 Markdown 单元格里记录:Python 版本、Scanpy 版本、numpy 版本、数据来源、分析日期。后面每次调参数,也顺手改一下说明文字。

如果想在代码里查看版本,可以:

import scanpy as sc sc.__version__

结合输出记录到 Notebook 里,几天后别人拿到你的文件也能知道当时用的环境。这个习惯在论文复现、团队协作和项目交接时特别重要。

7.3 遇到不懂的,优先看官方文档和源码

Scanpy 官方有非常完整的教程页面,常用绘图函数的参数说明也很清楚。遇到不确定的函数,先在 Notebook 里用Shift + Tab查看签名,再看官方示例。不要一遇到异常就整套重装环境,那只会掩盖真正的问题。

如果函数报错看不懂,直接在终端尝试打印帮助信息:

help(sc.pp.normalize_total)

最后说一个真实感受:单细胞分析的学习曲线其实不算陡,真正容易让人放弃的是“环境装不好、数据读不进、报错看不懂”这三件事。环境问题用独立 conda 环境解决,数据问题先从内置数据集练手,报错问题按照“内核、依赖、参数、数据”的顺序排查,大多数情况下都能定位到原因。先把一条流程跑稳,再谈批量、自动化和其他复杂分析,这条路对新手来说最省时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询