Python-生物信息学秘籍第三版-全-

Python 生物信息学秘籍第三版(全)

原文:annas-archive.org/md5/9694cf42f7d741c69225ff1cf52b0efe

译者:飞龙

协议:CC BY-NC-SA 4.0

第一章:《Python 生物信息学实用手册》第三版

使用现代 Python 库和应用程序解决现实世界的计算生物学问题

Tiago Antao

伯明翰—孟买

《Python 生物信息学实用手册》第三版

版权所有 © 2022 Packt Publishing

版权所有。未经出版商事先书面许可,本书的任何部分不得以任何形式或任何方式复制、存储在检索系统中或传输,除非是以简短的引文嵌入在重要的文章或评论中。

本书在编写过程中已尽最大努力确保所呈现信息的准确性。然而,本书所包含的信息是没有任何明示或暗示的担保的。无论是作者、Packt Publishing 还是其代理商与分销商,都不对因本书直接或间接造成的或声称已造成的任何损害负责。

Packt Publishing 力求通过适当使用大写字母提供本书中提到的所有公司和产品的商标信息,但无法保证这些信息的准确性。

出版产品经理:Devika Battike

高级编辑:David Sugarman

内容开发编辑:Joseph Sunil

技术编辑:Rahul Limbachiya

文案编辑:Safis Editing

项目协调员:Farheen Fathima

校对员:Safis Editing

索引员:Pratik Shirodkar

制作设计师:Shankar Kalbhor

市场协调员:Priyanka Mhatre

初版:2015 年 6 月

第二版:2018 年 11 月

第三版:2022 年 9 月

出版参考:1090922

由 Packt Publishing Ltd. 出版

Livery Place

35 Livery Street

伯明翰

B3 2PB,英国

ISBN 978-1-80323-642-1

www.packt.com

贡献者

关于作者

Tiago Antao 是一位生物信息学家,目前在基因组学领域工作。曾是计算机科学家,Tiago 拥有葡萄牙波尔图大学科学学院的生物信息学硕士学位,并在英国利物浦热带医学学院获得了关于抗药性疟疾传播的博士学位。博士后阶段,Tiago 在英国剑桥大学从事人类数据集研究,并在英国牛津大学处理蚊子全基因组测序数据,随后协助建立了美国蒙大拿大学的生物信息学基础设施。现在,他在马萨诸塞州波士顿的生物技术领域担任数据工程师。他是 Biopython 这一重要生物信息学 Python 包的共同作者之一。

关于审阅者

Urminder Singh 是一名生物信息学家、计算机科学家以及多个开源生物信息学工具的开发者。他的教育背景涵盖了物理学、计算机科学和计算生物学学位,包括在美国爱荷华州立大学获得的生物信息学博士学位。

他的研究兴趣广泛,包括新型基因进化、精准医学、社会基因组学、医学中的机器学习以及开发大规模异构数据的工具和算法。你可以在网上访问他:urmi-21.github.io

Tiffany Ho 在 Embark Veterinary 担任生物信息学助理。她拥有加利福尼亚大学戴维斯分校的遗传学和基因组学学士学位,并持有康奈尔大学的植物育种与遗传学硕士学位。

目录

前言

1

Python 与周边软件生态

使用 Anaconda 安装所需的基本软件

准备工作

如何实现...

更多内容...

使用 Docker 安装所需的软件

准备工作

如何实现...

另见

通过 rpy2 与 R 进行接口连接

准备工作

如何实现...

更多内容...

另见

在 Jupyter 中执行 R 魔法

准备工作

如何实现...

更多内容...

另见

2

了解 NumPy、pandas、Arrow 和 Matplotlib

使用 pandas 处理疫苗不良事件

准备工作

如何实现...

更多内容...

另见

处理 pandas DataFrame 联接的陷阱

准备工作

如何实现...

更多内容...

减少 pandas DataFrame 的内存使用

准备工作

如何实现…

另见

通过 Apache Arrow 加速 pandas 处理

准备工作

如何操作...

还有更多...

理解 NumPy 作为 Python 数据科学和生物信息学的引擎

准备工作

如何操作…

另请参阅

介绍 Matplotlib 进行图表生成

准备工作

如何操作...

还有更多...

另请参阅

3

下一代测序技术

访问 GenBank 和 NCBI 数据库移动

准备工作

如何操作...

还有更多...

另请参阅

执行基本序列分析

准备工作

如何操作...

还有更多...

另请参阅

使用现代序列格式

准备工作

如何操作...

还有更多...

另请参阅

处理对齐数据

准备工作

如何操作...

还有更多...

另请参阅

从 VCF 文件中提取数据

准备工作

如何操作...

还有更多...

另请参阅

研究基因组可访问性和过滤 SNP 数据

准备工作

如何操作...

还有更多...

另请参阅

使用 HTSeq 处理 NGS 数据

准备工作

如何操作...

还有更多...

4

高级 NGS 数据处理

准备数据集进行分析

准备工作

如何操作…

利用孟德尔误差信息进行质量控制

如何进行…

更多内容…

通过标准统计探索数据

如何进行…

更多内容…

从测序注释中查找基因组特征

如何进行…

更多内容…

使用 QIIME 2 Python API 进行宏基因组学分析

准备工作

如何进行...

更多内容...

5

与基因组合作

技术要求

使用高质量参考基因组进行工作

准备工作

如何进行...

更多内容...

另见

处理低质量基因组参考

准备工作

如何进行...

更多内容...

另见

遍历基因组注释

准备工作

如何进行...

更多内容...

另见

使用注释从参考基因组提取基因

准备工作

如何进行...

更多内容...

另见

使用 Ensembl REST API 查找同源基因

准备工作

如何进行...

更多内容...

从 Ensembl 检索基因本体信息

准备工作

如何进行...

更多内容...

另见

6

群体遗传学

准备工作

如何进行...

更多内容...

另见

使用 sgkit 进行群体遗传学分析,配合 xarray

准备工作

如何进行...

更多内容...

使用 sgkit 探索数据集

准备就绪

如何进行...

更多内容...

参见

分析种群结构

准备就绪

如何进行...

参见

执行 PCA

准备就绪

如何进行...

更多内容...

参见

通过混合分析调查种群结构

准备就绪

如何进行...

更多内容...

7

系统发育学

为系统发育分析准备数据集

准备就绪

如何进行...

更多内容...

参见

对齐遗传和基因组数据

准备就绪

如何进行...

比较序列

准备就绪

如何进行...

更多内容...

重建系统发育树

准备就绪

如何进行...

更多内容...

递归操作树形结构

准备就绪

如何进行...

更多内容...

可视化系统发育数据

准备就绪

如何进行...

更多内容...

8

使用蛋白质数据银行

在多个数据库中查找蛋白质

准备就绪

如何进行...

更多内容

介绍 Bio.PDB

准备就绪

如何进行...

还有更多

从 PDB 文件中提取更多信息

准备工作

如何操作...

在 PDB 文件中计算分子距离

准备工作

如何操作...

进行几何运算

准备工作

如何操作...

还有更多

使用 PyMOL 进行动画制作

准备工作

如何操作...

还有更多

使用 Biopython 解析 mmCIF 文件

准备工作

如何操作...

还有更多

9

生物信息学管道

介绍 Galaxy 服务器

准备工作

如何操作…

还有更多

通过 API 访问 Galaxy

准备工作

如何操作…

使用 Snakemake 部署变异分析管道

准备工作

如何操作…

还有更多

使用 Nextflow 部署变异分析管道

准备工作

如何操作…

还有更多

10

生物信息学中的机器学习

通过 PCA 示例介绍 scikit-learn

准备工作

如何操作...

还有更多...

使用 PCA 上的聚类来分类样本

准备工作

如何操作...

还有更多...

使用决策树探索乳腺癌特征

准备工作

如何操作...

使用随机森林预测乳腺癌结果

准备工作

如何操作…

更多内容...

11

使用 Dask 和 Zarr 进行并行处理

使用 Zarr 读取基因组数据

准备工作

如何做...

更多内容...

另见

使用 Python 多进程进行数据的并行处理

准备工作

如何做...

更多内容...

另见

使用 Dask 处理基于 NumPy 数组的基因组数据

准备工作

如何做...

更多内容...

另见

使用 dask.distributed 调度任务

准备工作

如何做...

更多内容...

另见

12

生物信息学中的函数式编程

理解纯函数

准备工作

如何做...

更多内容...

理解不可变性

准备工作

如何做...

更多内容...

避免可变性作为一种稳健的开发模式

准备工作

如何做...

更多内容...

使用懒加载编程进行管道化

准备工作

如何做...

更多内容...

Python 中递归的限制

准备工作

如何做...

更多内容...

Python functools 模块展示

准备工作

如何做...

更多内容...

另见...

索引

你可能会喜欢的其他书籍

前言

生物信息学是一个活跃的研究领域,使用各种简单到高级的计算方法从生物数据中提取有价值的信息,本书将展示如何使用 Python 管理这些任务。

本书的更新版《Python 生物信息学实用手册》首先概述了 Python 生态系统中各种工具和库,它们将帮助你转换、分析和可视化生物数据集。在后续章节中,你将学习下一代测序、单细胞分析、基因组学、宏基因组学、群体遗传学、系统发育学和蛋白质组学等关键技术,并通过实际案例深入了解。你将学习如何使用重要的管道系统,如 Galaxy 服务器和 Snakemake,以及如何理解 Python 中用于函数式和异步编程的各种模块。本书还将帮助你探索一些课题,比如在高性能计算框架(包括 Dask 和 Spark)下使用统计方法进行 SNP 发现,以及将机器学习算法应用于生物信息学领域。

本书结束时,你将掌握最新的编程技术和框架,能够处理各种规模的生物信息学数据。

本书适用对象

本书适用于生物信息学分析师、数据科学家、计算生物学家、研究人员以及希望解决中级到高级生物学和生物信息学问题的 Python 开发人员。读者需要具备 Python 编程语言的工作知识,具备基础生物学知识会有所帮助。

本书内容

第一章**,《Python 与相关软件生态》,告诉你如何使用 Python 搭建现代生物信息学环境。本章讨论了如何使用 Docker 部署软件,如何与 R 交互,以及如何使用 Jupyter Notebooks 进行互动。

第二章**,《了解 NumPy、pandas、Arrow 和 Matplotlib》,介绍了数据科学中的基础 Python 库:用于数组和矩阵处理的 NumPy;用于表格数据处理的 Pandas;优化 Pandas 处理的 Arrow,以及用于绘图的 Matplotlib。

第三章**,《下一代测序技术(NGS)》,提供了解决下一代测序数据的具体方案。本章教你如何处理大型的 FASTQ、BAM 和 VCF 文件,并讨论数据筛选。

第四章**,《高级 NGS 数据处理》,介绍了用于筛选 NGS 数据的高级编程技术。这包括使用孟德尔数据集,并通过标准统计方法进行分析。我们还介绍了宏基因组分析。

第五章**, 与基因组一起工作,不仅涉及高质量的参考数据——例如人类基因组——还讨论如何分析其他低质量的参考数据,通常见于非模式物种。本章介绍了 GFF 处理,教你分析基因组特征信息,并讨论如何使用基因本体论。

第六章**, 群体遗传学,描述了如何对实证数据集进行群体遗传学分析。例如,在 Python 中,我们可以执行主成分分析、计算 FST 或进行结构/混合图分析。

第七章**, 系统发育学,使用最近测序的埃博拉病毒的完整序列进行实际的系统发育分析,包括树的重建和序列比较。本章讨论了递归算法以处理树形结构。

第八章**, 使用蛋白质数据银行,重点介绍了 PDB 文件的处理,例如,执行蛋白质的几何分析。本章还介绍了蛋白质的可视化。

第九章**, 生物信息学管道,介绍了两种类型的管道。第一种管道是基于 Python 的 Galaxy,这是一个广泛使用的系统,具有 Web 界面,主要面向非编程用户,尽管生物信息学家仍可能需要以编程方式与其交互。第二种管道将基于 snakemake 和 nextflow,面向程序员的管道类型。

第十章**, 生物信息学中的机器学习,通过直观的方法介绍了如何使用机器学习解决计算生物学问题。本章涵盖了主成分分析、聚类、决策树和随机森林。

第十一章**, 使用 Dask 和 Zarr 进行并行处理,介绍了处理非常大数据集和计算密集型算法的技术。本章将解释如何跨多台计算机(集群或云)使用并行计算。我们还将讨论生物数据的高效存储。

第十二章**, 生物信息学中的函数式编程,介绍了函数式编程,允许开发更复杂的 Python 程序,借助惰性编程和不可变性,使得在复杂算法的并行环境中更容易部署。

从本书中获得最大收益

书中涵盖的软件/硬件 操作系统要求
Python 3.9 Windows、Mac OS X 和 Linux(推荐)
Numpy、Pandas、Matplolib
Biopython
Dask、zarr、scikit-learn

如果您正在使用本书的数字版,我们建议您自己输入代码或通过 GitHub 仓库访问代码(下一个章节中会提供链接)。这样可以帮助您避免与复制和粘贴代码相关的潜在错误。

下载示例代码文件

您可以从 GitHub 下载本书的示例代码文件,网址为github.com/PacktPublishing/Bioinformatics-with-Python-Cookbook-third-edition。如果代码有任何更新,它会在现有的 GitHub 仓库中更新。

我们还提供了来自丰富书籍和视频目录的其他代码包,您可以在github.com/PacktPublishing/找到。请查看!

下载彩色图像

我们还提供了一份 PDF 文件,其中包含本书中使用的截图/图表的彩色图像。您可以在此处下载:packt.link/3KQQO

使用的约定

本书中使用了许多文本约定。

文本中的代码:表示文本中的代码词汇、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名。以下是一个示例:“call_genotype 的形状是 56,241x1,1198,2,即它的维度是变异、样本、ploidy。”

代码块设置如下:

from Bio import SeqIO
genome_name = 'PlasmoDB-9.3_Pfalciparum3D7_Genome.fasta'
recs = SeqIO.parse(genome_name, 'fasta')
for rec in recs:
    print(rec.description)

当我们希望您注意到代码块中的某个部分时,相关的行或项目会以粗体显示:

AgamP4_2L | organism=Anopheles_gambiae_PEST | version=AgamP4 | length=49364325 | SO=chromosome
AgamP4_2R | organism=Anopheles_gambiae_PEST | version=AgamP4 | length=61545105 | SO=chromosome

粗体:表示新术语、重要词汇或屏幕上显示的词汇。例如,菜单或对话框中的文字通常以这种方式出现在文本中。以下是一个示例:“对于Chunk列,请参见 第十一章 —— 但目前您可以暂时忽略它。”

提示或重要说明

以这种方式显示。

部分

本书中,您会看到一些经常出现的标题(准备工作如何操作...工作原理...更多内容...另见)。

为了清晰地指导如何完成食谱,请按照以下方式使用这些部分:

准备工作

本节告诉您在食谱中可以期待什么,并描述如何设置所需的软件或进行任何前期设置。

如何操作…

本节包含了遵循食谱所需的步骤。

工作原理…

本节通常包含对上一节所做内容的详细解释。

更多内容…

本节包含有关食谱的额外信息,以帮助您更好地了解该食谱。

另见

本节提供了有助于理解食谱的其他有用信息链接。

联系我们

我们始终欢迎读者的反馈。

一般反馈:如果您对本书的任何部分有疑问,请在邮件主题中提及书名,并通过customercare@packtpub.com与我们联系。

勘误:尽管我们已尽力确保内容的准确性,但难免会有错误。如果你在本书中发现了错误,我们将非常感激你向我们报告。请访问 www.packtpub.com/support/errata,选择你的书籍,点击“勘误提交表单”链接,并填写详细信息。

盗版:如果你在互联网上发现任何我们作品的非法复制品,我们将感激不尽,如果你能提供该位置地址或网站名称。请通过 copyright@packt.com 联系我们,并附上材料的链接。

如果你有兴趣成为一名作者:如果你在某个领域有专业知识,并且有兴趣撰写或参与书籍的编写,请访问 authors.packtpub.com

评论

请留下评论。在阅读并使用本书后,为什么不在你购买书籍的网站上留下评论呢?潜在读者可以通过你的公正意见做出购买决定,我们在 Packt 可以了解你对我们产品的看法,而我们的作者也可以看到你对他们书籍的反馈。谢谢!

欲了解更多关于 Packt 的信息,请访问 packt.com

分享你的想法

一旦你阅读了《Python 生物信息学实用指南》,我们很希望听到你的想法!请点击这里直接前往本书的亚马逊评论页面,分享你的反馈。

你的评论对我们和技术社区都非常重要,它将帮助我们确保提供高质量的内容。

第二章:Python 与周边软件生态

我们将从安装本书大部分内容所需的基本软件开始。包括Python发行版,一些基本的 Python 库,以及外部生物信息学软件。在这里,我们还将关注 Python 之外的世界。在生物信息学和大数据领域,R 也是一个重要的角色;因此,你将学习如何通过 rpy2 与其进行交互,rpy2 是一个 Python/R 桥接工具。此外,我们将探索 IPython 框架(通过 Jupyter Lab)所能带来的优势,以便高效地与 R 进行接口。由于 Git 和 GitHub 在源代码管理中的广泛应用,我们将确保我们的设置能够与它们兼容。本章将为本书剩余部分的所有计算生物学工作奠定基础。

由于不同用户有不同的需求,我们将介绍两种安装软件的方式。一种方式是使用 Anaconda Python(docs.continuum.io/anaconda/)发行版,另一种方式是通过 Docker 安装软件(Docker 是基于容器共享同一操作系统内核的服务器虚拟化方法;请参考 https://www.docker.com/)。这种方法仍然会为你安装 Anaconda,但是在容器内安装。如果你使用的是 Windows 操作系统,强烈建议你考虑更换操作系统或使用 Windows 上现有的 Docker 选项。在 macOS 上,你可能能够原生安装大部分软件,但 Docker 也是可用的。使用本地发行版(如 Anaconda 或其他)学习比使用 Docker 更简单,但考虑到 Python 中的包管理可能较为复杂,Docker 镜像提供了更高的稳定性。

本章将涵盖以下内容:

  • 使用 Anaconda 安装所需的软件

  • 使用 Docker 安装所需的软件

  • 通过 rpy2 与 R 交互

  • 在 Jupyter 中使用 R 魔法

使用 Anaconda 安装所需的基本软件

在我们开始之前,我们需要安装一些基本的前置软件。接下来的章节将引导你完成所需软件及其安装步骤。每个章节和部分可能会有额外的要求,我们会在本书后续部分中明确说明。另一种启动方式是使用 Docker 配方,之后所有工作将通过 Docker 容器自动处理。

如果你已经在使用其他 Python 发行版,强烈建议考虑使用 Anaconda,因为它已经成为数据科学和生物信息学的事实标准。此外,它是可以让你从 Bioconda 安装软件的发行版(bioconda.github.io/)。

准备工作

Python 可以在不同的环境中运行。例如,你可以在 Java 虚拟机JVM)中使用 Python(通过 Jython 或使用 .NET 中的 IronPython)。然而,在这里,我们不仅关注 Python,还关注围绕它的完整软件生态系统。因此,我们将使用标准的(CPython)实现,因为 JVM 和 .NET 版本主要是与这些平台的原生库进行交互。

对于我们的代码,我们将使用 Python 3.10。如果你刚开始学习 Python 和生物信息学,任何操作系统都可以使用。但在这里,我们主要关注的是中级到高级的使用。所以,虽然你可能可以使用 Windows 和 macOS,但大多数繁重的分析将会在 Linux 上进行(可能是在 Linux 高性能计算HPC)集群上)。下一代测序NGS)数据分析和复杂的机器学习大多数是在 Linux 集群上进行的。

如果你使用的是 Windows,考虑升级到 Linux 来进行生物信息学工作,因为大多数现代生物信息学软件无法在 Windows 上运行。请注意,除非你计划使用计算机集群,否则 macOS 对几乎所有分析都是可以的,计算机集群通常基于 Linux。

如果你使用的是 Windows 或 macOS,并且没有轻松访问 Linux 的方式,别担心。现代虚拟化软件(如 VirtualBoxDocker)将会帮你解决问题,它们允许你在操作系统上安装一个虚拟的 Linux。如果你正在使用 Windows 并决定使用原生方式而不是 Anaconda,要小心选择库;如果你为所有内容(包括 Python 本身)安装 32 位版本,可能会更安全。

注意

如果你使用的是 Windows,许多工具将无法使用。

生物信息学和数据科学正在以惊人的速度发展,这不仅仅是炒作,而是现实。在安装软件库时,选择版本可能会很棘手。根据你所使用的代码,某些旧版本可能无法工作,或者可能甚至无法与新版本兼容。希望你使用的任何代码会指出正确的依赖项——尽管这并不保证。在本书中,我们将固定所有软件包的精确版本,并确保代码可以与这些版本一起工作。代码可能需要根据其他软件包版本进行调整,这是非常自然的。

本书开发的软件可以在 github.com/PacktPublishing/Bioinformatics-with-Python-Cookbook-third-edition 找到。要访问它,你需要安装 Git。习惯使用 Git 可能是个好主意,因为许多科学计算软件都是在它的基础上开发的。

在正确安装 Python 环境之前,你需要先安装所有与 Python 交互的外部非 Python 软件。这个列表会根据章节有所不同,所有章节特定的软件包将在相应的章节中解释。幸运的是,自本书的前几个版本以来,大多数生物信息学软件已经可以通过 Bioconda 项目安装,因此安装通常很容易。

你需要安装一些开发编译器和库,所有这些都是免费的。在 Ubuntu 上,建议安装 build-essential 包(apt-get install build-essential),在 macOS 上,建议安装 Xcodedeveloper.apple.com/xcode/)。

在下表中,你将找到开发生物信息学所需的最重要的 Python 软件列表:

名称 应用 网址 目的
Project Jupyter 所有章节 https://jupyter.org/ 交互式计算
pandas 所有章节 https://pandas.pydata.org/ 数据处理
NumPy 所有章节 http://www.numpy.org/ 数组/矩阵处理
SciPy 所有章节 https://www.scipy.org/ 科学计算
Biopython 所有章节 https://biopython.org/ 生物信息学库
seaborn 所有章节 http://seaborn.pydata.org/ 统计图表库
R 生物信息学与统计 https://www.r-project.org/ 统计计算语言
rpy2 R 连接 https://rpy2.readthedocs.io R 接口
PyVCF NGS https://pyvcf.readthedocs.io VCF 处理
Pysam NGS https://github.com/pysam-developers/pysam SAM/BAM 处理
HTSeq NGS/基因组 https://htseq.readthedocs.io NGS 处理
DendroPY 系统发育学 https://dendropy.org/ 系统发育学
PyMol 蛋白质组学 https://pymol.org 分子可视化
scikit-learn 机器学习 http://scikit-learn.org 机器学习库
Cython 大数据 http://cython.org/ 高性能
Numba 大数据 https://numba.pydata.org/ 高性能
Dask 大数据 http://dask.pydata.org 并行处理

图 1.1 – 显示在生物信息学中有用的各种软件包的表格

我们将使用 pandas 来处理大多数表格数据。另一种选择是仅使用标准 Python。pandas 在数据科学中已经变得非常普及,因此如果数据量适合内存,使用它处理所有表格数据可能是明智之举。

所有的工作将在 Jupyter 项目内进行,即 Jupyter Lab。Jupyter 已成为编写交互式数据分析脚本的事实标准。不幸的是,Jupyter Notebooks 的默认格式基于 JSON。这种格式难以阅读、难以比较,并且需要导出才能输入到普通的 Python 解释器中。为了解决这个问题,我们将使用jupytext (jupytext.readthedocs.io/) 扩展 Jupyter,允许我们将 Jupyter 笔记本保存为普通的 Python 程序。

怎么做...

要开始,请查看以下步骤:

  1. 首先从 https://www.anaconda.com/products/individual 下载 Anaconda 发行版。我们将使用版本 21.05,尽管您可能可以使用最新版本。您可以接受安装的所有默认设置,但您可能希望确保conda二进制文件在您的路径中(不要忘记打开一个新窗口,以便路径可以更新)。如果您有另一个 Python 发行版,请注意您的PYTHONPATH和现有的 Python 库。最好卸载所有其他 Python 版本和已安装的 Python 库。尽可能地,删除所有其他 Python 版本和已安装的 Python 库。

  2. 让我们继续使用库。我们现在将使用以下命令创建一个名为bioinformatics_base的新conda环境,并安装biopython=1.70

    conda create -n bioinformatics_base python=3.10
    
  3. 让我们按以下步骤激活环境:

    conda activate bioinformatics_base
    
  4. 让我们将biocondaconda-forge通道添加到我们的源列表:

    conda config --add channels bioconda
    conda config --add channels conda-forge
    
  5. 还要安装基本软件包:

    conda install \
    biopython==1.79 \
    jupyterlab==3.2.1 \
    jupytext==1.13 \
    matplotlib==3.4.3 \
    numpy==1.21.3 \
    pandas==1.3.4 \
    scipy==1.7.1
    
  6. 现在,让我们保存我们的环境,以便以后在其他机器上创建新的环境,或者如果需要清理基础环境:

    conda list –explicit > bioinformatics_base.txt
    
  7. 我们甚至可以从conda安装 R:

    conda install rpy2 r-essentials r-gridextra
    

请注意,r-essentials会安装许多 R 包,包括后面要用到的 ggplot2。另外,我们还会安装r-gridextra,因为我们在 Notebook 中会用到它。

还有更多...

如果您不喜欢使用 Anaconda,您可以选择使用任何发行版通过pip安装许多 Python 库。您可能需要许多编译器和构建工具,不仅包括 C 编译器,还包括 C++和 Fortran。

我们将不再使用前面步骤中创建的环境。相反,我们将把它用作克隆工作环境的基础。这是因为使用 Python 进行环境管理,即使借助conda包系统的帮助,仍可能非常痛苦。因此,我们将创建一个干净的环境,以免造成损害,并可以从中派生出开发环境,如果开发环境变得难以管理。

例如,假设您想创建一个带有scikit-learn的机器学习环境。您可以执行以下操作:

  1. 使用以下命令创建原始环境的克隆:

    conda create -n scikit-learn --clone bioinformatics_base
    
  2. 添加scikit-learn

    conda activate scikit-learn
    conda install scikit-learn
    

在 JupyterLab 中,我们应该通过 notebook 打开我们的 jupytext 文件,而不是通过文本编辑器。由于 jupytext 文件与 Python 文件具有相同的扩展名——这是一个特性,而非 bug——JupyterLab 默认会使用普通的文本编辑器。当我们打开 jupytext 文件时,我们需要覆盖默认设置。右键点击并选择 Notebook,如下图所示:

图 1.2 – 在 Notebook 中打开 jupytext 文件

图 1.2 – 在 Notebook 中打开 jupytext 文件

我们的 jupytext 文件将不会保存图形输出,这对于本书来说已经足够。如果你想要一个带有图片的版本,这是可以通过配对的 notebooks 实现的。更多详情,请查看 Jupytext 页面 (github.com/mwouts/jupytext)。

警告

由于我们的代码是为了在 Jupyter 中运行的,本书中很多地方,我不会使用 print 输出内容,因为单元格的最后一行会被自动渲染。如果你没有使用 notebook,请记得使用 print

使用 Docker 安装所需的软件

Docker 是实现操作系统级虚拟化的最广泛使用的框架。这项技术使你能够拥有一个独立的容器:一个比虚拟机更轻量的层,但仍然允许你将软件进行隔离。这基本上隔离了所有进程,使得每个容器看起来像一个虚拟机。

Docker 在开发领域的两端表现得非常好:它是设置本书内容以用于学习目的的便捷方式,并且可以成为你在复杂环境中部署应用程序的首选平台。这个方案是前一个方案的替代。

然而,对于长期的开发环境,类似前一个方案的方法可能是你的最佳选择,尽管它可能需要更繁琐的初始设置。

准备工作

如果你使用的是 Linux,首先需要做的是安装 Docker。最安全的解决方案是从 www.docker.com/ 获取最新版本。虽然你的 Linux 发行版可能有 Docker 包,但它可能过时且有 bug。

如果你使用的是 Windows 或 macOS,不要灰心;查看 Docker 网站。有多种可供选择的方案,但没有明确的公式,因为 Docker 在这些平台上发展得很快。你需要一台相对较新的计算机来运行我们的 64 位虚拟机。如果遇到任何问题,重启计算机并确保 BIOS、VT-X 或 AMD-V 已启用。至少你需要 6 GB 内存,最好更多。

注意

这将需要从互联网进行非常大的下载,因此请确保你有足够的带宽。另外,准备好等待很长时间。

如何操作...

要开始,请按照以下步骤操作:

  1. 在 Docker shell 中使用以下命令:

    docker build -t bio https://raw.githubusercontent.com/PacktPublishing/Bioinformatics-with-Python-Cookbook-third-edition/main/docker/main/Dockerfile
    

在 Linux 上,你需要有 root 权限或被加入到 Docker Unix 组中。

  1. 现在你准备好运行容器,具体如下:

    docker run -ti -p 9875:9875 -v YOUR_DIRECTORY:/data bio
    
  2. YOUR_DIRECTORY 替换为你操作系统中的一个目录。该目录将在主机操作系统和 Docker 容器之间共享。YOUR_DIRECTORY 在容器内会显示为 /data,反之亦然。

-p 9875:9875 将暴露容器的 TCP 端口 9875 到主机计算机的端口 9875

特别是在 Windows 上(也许在 macOS 上),请确保你的目录在 Docker shell 环境中确实可见。如果不可见,请查看官方 Docker 文档,了解如何暴露目录。

  1. 现在你准备好使用系统了。将浏览器指向 http://localhost:9875,你应该会看到 Jupyter 环境。

如果在 Windows 上无法运行,请查看官方 Docker 文档 (docs.docker.com/),了解如何暴露端口。

另请参见

以下内容也值得了解:

  • Docker 是最广泛使用的容器化软件,最近在使用量上增长巨大。你可以在 www.docker.com/ 阅读更多关于它的信息。

  • 作为 Docker 的一个面向安全的替代方案,rkt 可以在 coreos.com/rkt/ 找到。

  • 如果你无法使用 Docker,例如,如果你没有必要的权限(这通常发生在大多数计算集群中),可以查看 Singularity,网址为 www.sylabs.io/singularity/

通过 rpy2 与 R 进行交互

如果你需要某些功能,但在 Python 库中找不到,首先应该检查该功能是否已经在 R 中实现。对于统计方法,R 仍然是最完整的框架;此外,某些生物信息学功能在 R 中可用,并且可能作为 Bioconductor 项目的一部分提供。

ggplot2,我们将从人类 1,000 基因组计划 (www.1000genomes.org/) 下载它的元数据。这本书不是关于 R 的,但我们希望提供有趣且实用的示例。

准备工作

你需要从 1,000 基因组序列索引中获取元数据文件。请查看 github.com/PacktPublishing/Bioinformatics-with-Python-Cookbook-third-edition/blob/main/Datasets.py,并下载 sequence.index 文件。如果你使用 Jupyter Notebook,打开 Chapter01/Interfacing_R.py 文件,并直接执行顶部的 wget 命令。

该文件包含了项目中所有 FASTQ 文件的信息(在接下来的章节中,我们将使用来自人类 1,000 基因组项目的数据)。这包括 FASTQ 文件、样本 ID、来源人群以及每条数据通道的重要统计信息,例如读取数和读取的 DNA 序列数。

要设置 Anaconda,可以运行以下命令:

conda create -n bioinformatics_r --clone bioinformatics_base
conda activate bioinformatics_r
conda install r-ggplot2=3.3.5 r-lazyeval r-gridextra rpy2

使用 Docker,你可以运行以下命令:

docker run -ti -p 9875:9875 -v YOUR_DIRECTORY:/data tiagoantao/bioinformatics_r

现在我们可以开始了。

如何操作...

开始之前,请按照以下步骤操作:

  1. 让我们从导入一些库开始:

    import os
    from IPython.display import Image
    import rpy2.robjects as robjects
    import rpy2.robjects.lib.ggplot2 as ggplot2
    from rpy2.robjects.functions import SignatureTranslatedFunction
    import pandas as pd
    import rpy2.robjects as ro
    from rpy2.robjects import pandas2ri
    from rpy2.robjects import local_converter
    

我们将在 Python 端使用pandas。R 中的 DataFrame 与pandas非常匹配。

  1. 我们将使用 R 的read.delim函数从文件中读取数据:

    read_delim = robjects.r('read.delim')
    seq_data = read_delim('sequence.index', header=True, stringsAsFactors=False)
    #In R:
    # seq.data <- read.delim('sequence.index', header=TRUE, stringsAsFactors=FALSE)
    

导入后,我们做的第一件事是访问 R 中的read.delim函数,它允许你读取文件。R 语言规范允许在对象名称中使用点。因此,我们必须将函数名转换为read_delim。然后,我们调用正确的函数名;请注意以下几个显著的特性。首先,大多数原子对象(如字符串)可以直接传递而无需转换。其次,参数名也可以无缝转换(除了点问题)。最后,对象可在 Python 命名空间中使用(然而,对象实际上不能在 R 命名空间中使用;我们稍后会详细讨论)。

作为参考,我附上了相应的 R 代码。我希望你能看出它的转换是非常简单的。seq_data对象是一个 DataFrame。如果你了解基本的 R 或者pandas,你可能对这种数据结构有所了解。如果不了解,那么它基本上是一个表格,即一系列行,其中每一列都具有相同的数据类型。

  1. 让我们对这个 DataFrame 进行基本的检查,如下所示:

    print('This dataframe has %d columns and %d rows' %
    (seq_data.ncol, seq_data.nrow))
    print(seq_data.colnames)
    #In R:
    # print(colnames(seq.data))
    # print(nrow(seq.data))
    # print(ncol(seq.data))
    

再次注意代码的相似性。

  1. 你甚至可以使用以下代码混合不同的风格:

    my_cols = robjects.r.ncol(seq_data)
    print(my_cols)
    

你可以直接调用 R 函数;在这种情况下,如果函数名称中没有点,我们将调用ncol;但是要小心。这样做会输出结果,而不是 26(列数),而是[26],这是一个包含26元素的向量。这是因为默认情况下,R 中的大多数操作返回向量。如果你想要列数,必须执行my_cols[0]。另外,谈到陷阱,注意 R 数组的索引是从 1 开始的,而 Python 是从 0 开始的。

  1. 现在,我们需要进行一些数据清理。例如,有些列应该解释为数字,但它们却被当作字符串读取:

    as_integer = robjects.r('as.integer')
    match = robjects.r.match
    my_col = match('READ_COUNT', seq_data.colnames)[0] # vector returned
    print('Type of read count before as.integer: %s' % seq_data[my_col - 1].rclass[0])
    seq_data[my_col - 1] = as_integer(seq_data[my_col - 1])
    print('Type of read count after as.integer: %s' % seq_data[my_col - 1].rclass[0])
    

match函数与 Python 列表中的index方法有些相似。正如预期的那样,它返回一个向量,因此我们可以提取0元素。它也是从 1 开始索引的,因此在 Python 中工作时需要减去 1。as_integer函数会将一列数据转换为整数。第一次打印会显示字符串(即被"包围的值),而第二次打印会显示数字。

  1. 我们需要对这张表进行更多处理;有关详细信息可以在笔记本中找到。在这里,我们将完成将 DataFrame 获取到 R 中的操作(请记住,虽然它是一个 R 对象,但实际上在 Python 命名空间中可见):

    robjects.r.assign('seq.data', seq_data)
    

这将在 R 命名空间中创建一个名为seq.data的变量,其内容来自 Python 命名空间中的 DataFrame。请注意,在此操作之后,两个对象将是独立的(如果更改其中一个对象,将不会反映到另一个对象中)。

注意

尽管你可以在 Python 上执行绘图,R 默认内置了绘图功能(这里我们将忽略)。它还有一个叫做ggplot2的库,实现了图形语法(一种声明性语言,用于指定统计图表)。

  1. 关于我们基于人类 1,000 个基因组计划的具体示例,首先,我们将绘制一个直方图,显示生成所有测序通道的中心名称的分布。为此,我们将使用ggplot

    from rpy2.robjects.functions import SignatureTranslatedFunction
    ggplot2.theme = SignatureTranslatedFunction(ggplot2.theme, init_prm_translate = {'axis_text_x': 'axis.text.x'})
    bar = ggplot2.ggplot(seq_data) + ggplot2.geom_bar() + ggplot2.aes_string(x='CENTER_NAME') + ggplot2.theme(axis_text_x=ggplot2.element_text(angle=90, hjust=1))
    robjects.r.png('out.png', type='cairo-png')
    bar.plot()
    dev_off = robjects.r('dev.off')
    dev_off()
    

第二行有点无趣,但是是重要的样板代码的一部分。我们将调用的一个 R 函数具有其名称中带有点的参数。由于 Python 函数调用中不能有这个点,我们必须将axis.text.x的 R 参数名称映射到axis_text_r的 Python 名称中的函数主题中。我们对其进行了修补(即,我们用其自身的修补版本替换了ggplot2.theme)。

然后,我们绘制图表本身。请注意ggplot2的声明性质,因为我们向图表添加特性。首先,我们指定seq_data数据框,然后使用称为geom_bar的直方图条形图。接下来,我们注释x变量(CENTER_NAME)。最后,通过更改主题,我们旋转x 轴的文本。我们通过关闭 R 打印设备来完成这一操作。

  1. 现在,我们可以在 Jupyter Notebook 中打印图像:

    Image(filename='out.png')
    

生成以下图表:

图 1.3 – 使用 ggplot2 生成的中心名称直方图,负责从 1,000 个基因组计划中测序人类基因组数据的通道

图 1.3 – 使用 ggplot2 生成的中心名称直方图,负责从 1,000 个基因组计划中测序人类基因组数据的通道

  1. 作为最后一个示例,我们现在将对 Yoruban(YRI)和来自北欧和西欧的犹他州居民(CEU)的所有测序通道的读取和碱基计数进行散点图绘制,使用人类 1,000 个基因组计划(该项目的数据总结,我们将充分使用,可以在第三章使用现代序列格式食谱中看到)。此外,我们对不同类型的测序之间的差异感兴趣(例如,外显子覆盖率、高覆盖率和低覆盖率)。首先,我们生成一个仅包含YRICEU通道的 DataFrame,并限制最大碱基和读取计数:

    robjects.r('yri_ceu <- seq.data[seq.data$POPULATION %in% c("YRI", "CEU") & seq.data$BASE_COUNT < 2E9 & seq.data$READ_COUNT < 3E7, ]')
    yri_ceu = robjects.r('yri_ceu')
    
  2. 现在我们已经准备好绘图了:

    scatter = ggplot2.ggplot(yri_ceu) + ggplot2.aes_string(x='BASE_COUNT', y='READ_COUNT', shape='factor(POPULATION)', col='factor(ANALYSIS_GROUP)') + ggplot2.geom_point()
    robjects.r.png('out.png')
    scatter.plot()
    

希望这个例子(请参考下面的截图)能清楚地展示图形语法方法的强大。我们将首先声明 DataFrame 和正在使用的图表类型(即通过 geom_point 实现的散点图)。

注意到很容易表达每个点的形状取决于 POPULATION 变量,而颜色取决于 ANALYSIS_GROUP 变量:

图 1.4 – 由 ggplot2 生成的散点图,显示所有测序通道的基础和读取计数;每个点的颜色和形状反映了类别数据(种群和所测序的数据类型)

图 1.4 – 由 ggplot2 生成的散点图,显示所有测序通道的基础和读取计数;每个点的颜色和形状反映了类别数据(种群和所测序的数据类型)

  1. 由于 R DataFrame 与 pandas 非常相似,因此在两者之间进行转换是很有意义的,因为 rpy2 支持这种转换:

    import rpy2.robjects as ro
    from rpy2.robjects import pandas2ri
    from rpy2.robjects.conversion import localconverter 
    with localconverter(ro.default_converter + pandas2ri.converter):
      pd_yri_ceu = ro.conversion.rpy2py(yri_ceu)
    del pd_yri_ceu['PAIRED_FASTQ']
    with localconverter(ro.default_converter + pandas2ri.converter):
      no_paired = ro.conversion.py2rpy(pd_yri_ceu)
    robjects.r.assign('no.paired', no_paired)
    robjects.r("print(colnames(no.paired))")
    

我们首先导入必要的转换模块 —— rpy2 提供了许多将数据从 R 转换到 Python 的策略。这里,我们关注的是数据框的转换。然后,我们转换 R DataFrame(请注意,我们正在转换 R 命名空间中的 yri_ceu,而不是 Python 命名空间中的)。我们删除了 pandas DataFrame 中指示配对 FASTQ 文件名称的列,并将其复制回 R 命名空间。如果你打印新 R DataFrame 的列名,你会发现 PAIRED_FASTQ 列丢失了。

还有更多内容...

值得重复的是,Python 软件生态的进展速度非常快。这意味着如果今天某个功能不可用,它可能在不久的将来会发布。因此,如果你正在开发一个新项目,务必在使用 R 包中的功能之前,先检查 Python 领域的最新进展。

Bioconductor 项目中有许多适用于生物信息学的 R 包(www.bioconductor.org/)。这应该是你在 R 世界中进行生物信息学功能开发的首选。不过,需要注意的是,许多 R 生物信息学包并不在 Bioconductor 上,因此务必在 综合 R 存档网络 (CRAN) 中查找更广泛的 R 包(请参阅 CRAN:cran.rproject.org/)。

Python 中有许多绘图库。Matplotlib 是最常用的库,但你也可以选择其他众多的库。在 R 的背景下,值得注意的是,Python 中有一个类似于 ggplot2 的实现,它基于图形语法描述语言,用于图表的绘制,惊讶吗?这就是 ggplot!(yhat.github.io/ggpy/)。

另见

要了解更多关于这些主题的信息,请参考以下资源:

  • 有许多关于 R 的教程和书籍;请访问 R 的官方网站 (www.r-project.org/) 查看文档。

  • 对于 Bioconductor,请查看文档:manuals.bioinformatics.ucr.edu/home/R_BioCondManual

  • 如果你从事 NGS 工作,可能还想查看 Bioconductor 中的高通量序列分析:manuals.bioinformatics.ucr.edu/home/ht-seq

  • rpy库的文档是你通往 R 的 Python 接口,文档可以在rpy2.bitbucket.io/找到。

  • 图形语法方法在 Leland Wilkinson 所著的书《图形语法》中有详细描述,由 Springer 出版。

  • 在数据结构方面,可以在pandas库中找到类似于 R 的功能。你可以在pandas.pydata.org/pandas-docs/dev/tutorials.xhtml找到一些教程。Wes McKinney 的《Python 数据分析》一书(由 O'Reilly Media 出版)也是一个可以考虑的替代方案。在下一章中,我们将讨论 pandas,并在整本书中使用它。

在 Jupyter 中执行 R 魔法

Jupyter 相比标准 Python 提供了许多额外的功能。在这些功能中,它提供了一种名为魔法的可扩展命令框架(实际上,这只适用于 Jupyter 的 IPython 内核,因为它本质上是 IPython 的一个功能,但这正是我们关注的内容)。魔法命令允许你以许多有用的方式扩展语言。有一些魔法函数可以用于处理 R。如你在示例中将看到的,这使得与 R 的接口更加简洁和声明式。本配方不会引入任何新的 R 功能,但希望能清楚地说明,IPython 如何在科学计算中为提高生产力提供重要帮助。

准备工作

你需要遵循通过 rpy2 与 R 接口配方中的前期准备步骤。笔记本是Chapter01/R_magic.py。该笔记本比这里呈现的配方更完整,包含更多的图表示例。为了简洁起见,我们将只集中介绍与 R 交互的基本构造。如果你使用 Docker,可以使用以下命令:

docker run -ti -p 9875:9875 -v YOUR_DIRECTORY:/data tiagoantao/bioinformatics_r

操作方法...

本配方是对前一个配方的大胆简化,它展示了 R 魔法的简洁性和优雅性:

  1. 首先需要做的是加载 R 魔法和ggplot2

    import rpy2.robjects as robjects
    import rpy2.robjects.lib.ggplot2 as ggplot2
    %load_ext rpy2.ipython
    

请注意,%符号表示一个 IPython 特定的指令。举个简单的例子,你可以在 Jupyter 单元格中写入%R print(c(1, 2))

查看一下如何无需使用robjects包就能轻松执行 R 代码。实际上,rpy2被用来查看背后的实现。

  1. 让我们读取在之前配方中下载的sequence.index文件:

    %%R
    seq.data <- read.delim('sequence.index', header=TRUE, stringsAsFactors=FALSE)
    seq.data$READ_COUNT <- as.integer(seq.data$READ_COUNT)
    seq.data$BASE_COUNT <- as.integer(seq.data$BASE_COUNT)
    

然后,您可以使用 %%R 来指定整个单元格应该被解释为 R 代码(注意双 %%)。

  1. 现在,我们可以将变量传递到 Python 命名空间:

    seq_data = %R seq.data
    print(type(seq_data))  # pandas dataframe!
    

DataFrame 的类型不是标准的 Python 对象,而是一个 pandas DataFrame。这与之前版本的 R magic 接口有所不同。

  1. 由于我们有一个 pandas DataFrame,我们可以通过 pandas 接口轻松操作它:

    my_col = list(seq_data.columns).index("CENTER_NAME")
    seq_data['CENTER_NAME'] = seq_data['CENTER_NAME'].apply(lambda` x: x.upper())
    
  2. 让我们将这个 DataFrame 重新放入 R 命名空间,如下所示:

    %R -i seq_data
    %R print(colnames(seq_data))
    

-i 参数通知 magic 系统,将后面的变量从 Python 空间复制到 R 命名空间。第二行仅显示 DataFrame 确实在 R 中可用。我们使用的名称与原始名称不同——它是 seq_data,而不是 seq.data

  1. 让我们进行最后的清理(有关更多详细信息,请参见之前的食谱),并打印出与之前相同的条形图:

    %%R
    bar <- ggplot(seq_data) +  aes(factor(CENTER_NAME)) + geom_bar() + theme(axis.text.x = element_text(angle = 90, hjust = 1))
    print(bar)
    

此外,R magic 系统允许您减少代码量,因为它改变了 R 与 IPython 交互的行为。例如,在前一个食谱中的 ggplot2 代码中,您无需使用 .pngdev.off R 函数,因为 magic 系统会为您处理这些。当您告诉 R 打印图表时,它会神奇地出现在您的笔记本或图形控制台中。

还有更多内容...

随着时间的推移,R magic 的接口似乎发生了很大的变化。例如,我已经多次更新了本书第一版中的 R 代码。当前版本的 DataFrame 赋值返回 pandas 对象,这是一个重大变化。

另请参见

欲了解更多信息,请查看以下链接:

第三章:了解 NumPy、pandas、Arrow 和 Matplotlib

Python 的最大优势之一是其丰富的高质量科学和数据处理库。所有这些库的核心是NumPy,它提供了高效的数组和矩阵支持。在 NumPy 之上,我们几乎可以找到所有的科学库。例如,在我们这一领域,有Biopython。但其他通用的数据分析库也可以在我们这一领域使用。例如,pandas 是处理表格数据的事实标准。最近,Apache Arrow 提供了一些 pandas 功能的高效实现,并且支持语言互操作性。最后,Matplotlib 是 Python 领域中最常见的绘图库,适用于科学计算。虽然这些库都是广泛应用的通用库,但它们对生物信息学处理至关重要,因此我们将在本章中学习它们。

我们将从 pandas 开始,因为它提供了一个高层次的库,具有非常广泛的实际应用性。然后,我们将介绍 Arrow,我们只在支持 pandas 的范围内使用它。接下来,我们将讨论 NumPy,这是几乎所有工作背后的驱动力。最后,我们将介绍 Matplotlib。

我们的教程非常基础——这些库中的每一个都可以轻松占据一本完整的书,但这些教程应该足够帮助你完成本书的内容。如果你使用 Docker,并且由于所有这些库对于数据分析至关重要,它们可以在来自第一章tiagoantao/bioinformatics_base Docker 镜像中找到。

在本章中,我们将涵盖以下教程:

  • 使用 pandas 处理疫苗不良事件

  • 处理 pandas DataFrame 合并的陷阱

  • 降低 pandas DataFrame 的内存使用

  • 使用 Apache Arrow 加速 pandas 处理

  • 理解 NumPy 作为 Python 数据科学和生物信息学的引擎

  • 介绍 Matplotlib 用于图表生成

使用 pandas 处理疫苗不良事件

我们将通过一个具体的生物信息学数据分析示例来介绍 pandas:我们将研究来自疫苗不良事件报告系统VAERSvaers.hhs.gov/)的数据。VAERS 由美国卫生与公共服务部维护,包含自 1990 年以来的疫苗不良事件数据库。

VAERS 提供的数据是逗号分隔值CSV)格式。CSV 格式非常简单,甚至可以用简单的文本编辑器打开(请注意,文件过大会导致编辑器崩溃),或者使用类似 Excel 的电子表格程序打开。pandas 可以非常轻松地处理这种格式。

准备工作

首先,我们需要下载数据。可以在vaers.hhs.gov/data/datasets.xhtml下载。请下载 ZIP 文件:我们将使用 2021 年文件,不要仅下载单个 CSV 文件。下载文件后,解压缩它,然后使用gzip –9 *csv将所有文件单独重新压缩,以节省磁盘空间。

随时可以使用文本编辑器查看文件,或者最好使用诸如less(压缩文件用zless)的工具。您可以在vaers.hhs.gov/docs/VAERSDataUseGuide_en_September2021.pdf找到文件内容的文档。

如果您使用的是笔记本,代码已在开头提供,您可以处理所需的处理步骤。如果您使用的是 Docker,基础镜像已足够。

代码可以在Chapter02/Pandas_Basic.py中找到。

如何做到这一点...

请按照以下步骤操作:

  1. 让我们从加载主要数据文件并收集基本统计信息开始:

    vdata = pd.read_csv(
        "2021VAERSDATA.csv.gz", encoding="iso-8859-1")
    vdata.columns
    vdata.dtypes
    vdata.shape
    

我们首先加载数据。在大多数情况下,默认的 UTF-8 编码就可以正常工作,但在这种情况下,文本编码是legacy iso-8859-1。接下来,我们打印列名,列名以VAERS_IDRECVDATESTATEAGE_YRS等开头,共有 35 个条目,分别对应每一列。然后,我们打印每列的类型。以下是前几个条目:

VAERS_ID          int64
RECVDATE         object
STATE            object
AGE_YRS         float64
CAGE_YR         float64
CAGE_MO         float64
SEX              object

通过这样做,我们获得了数据的形状:(654986, 35)。这意味着有 654,986 行和 35 列。您可以使用上述任何一种策略来获取有关表格元数据的信息。

  1. 现在,让我们探索数据:

    vdata.iloc[0]
    vdata = vdata.set_index("VAERS_ID")
    vdata.loc[916600]
    vdata.head(3)
    vdata.iloc[:3]
    vdata.iloc[:5, 2:4]
    

我们可以通过多种方式查看数据。我们将从根据位置检查第一行开始。以下是简化版本:

VAERS_ID                                       916600
RECVDATE                                       01/01/2021
STATE                                          TX
AGE_YRS                                        33.0
CAGE_YR                                        33.0
CAGE_MO                                        NaN
SEX                                            F
…
TODAYS_DATE                                          01/01/2021
BIRTH_DEFECT                                  NaN
OFC_VISIT                                     Y
ER_ED_VISIT                                       NaN
ALLERGIES                                       Pcn and bee venom

在按VAERS_ID索引后,我们可以使用一个 ID 来获取一行。我们可以使用 916600(这是前一条记录的 ID)并获得相同的结果。

然后,我们提取前三行。注意我们可以通过两种不同的方式做到这一点:

  • 使用head方法

  • 使用更通用的数组规范;也就是iloc[:3]

最后,我们提取前五行,但仅提取第二和第三列——iloc[:5, 2:4]。以下是输出:

          AGE_YRS  CAGE_YR
VAERS_ID                  
916600       33.0     33.0
916601       73.0     73.0
916602       23.0     23.0
916603       58.0     58.0
916604       47.0     47.0
  1. 现在,让我们做一些基本的计算,即计算数据集中最大年龄:

    vdata["AGE_YRS"].max()
    vdata.AGE_YRS.max()
    

最大值为 119 岁。比结果更重要的是,注意两种访问AGE_YRS的方式(作为字典键和作为对象字段)来访问列。

  1. 现在,让我们绘制涉及的年龄分布:

    vdata["AGE_YRS"].sort_values().plot(use_index=False)
    vdata["AGE_YRS"].plot.hist(bins=20) 
    

这将生成两个图表(以下步骤显示的是简化版本)。我们在这里使用的是 pandas 的绘图工具,它底层使用 Matplotlib。

  1. 虽然我们已经有完整的 Matplotlib 绘图食谱(引入 Matplotlib 进行图表生成),但让我们在此先通过直接使用它来一窥究竟:

    import matplotlib.pylot as plt
    fig, ax = plt.subplots(1, 2, sharey=True)
    fig.suptitle("Age of adverse events")
    vdata["AGE_YRS"].sort_values().plot(
        use_index=False, ax=ax[0],
        xlabel="Obervation", ylabel="Age")
    vdata["AGE_YRS"].plot.hist(bins=20, orientation="horizontal")
    

这包括前一步的两个图表。以下是输出:

图 2.1 – 左侧 – 每个不良反应观察的年龄;右侧 – 显示年龄分布的直方图

图 2.1 – 左侧 – 每个不良反应观察的年龄;右侧 – 显示年龄分布的直方图

  1. 我们也可以采取一种非图形的、更分析性的方法,比如按年计数事件:

    vdata["AGE_YRS"].dropna().apply(lambda x: int(x)).value_counts()
    

输出结果如下:

50     11006
65     10948
60     10616
51     10513
58     10362
      ...
  1. 现在,让我们看看有多少人死亡:

    vdata.DIED.value_counts(dropna=False)
    vdata["is_dead"] = (vdata.DIED == "Y")
    

计数结果如下:

NaN    646450
Y        8536
Name: DIED, dtype: int64

注意,DIED 的类型不是布尔值。使用布尔值表示布尔特性更具声明性,因此我们为它创建了 is_dead

提示

在这里,我们假设 NaN 应该被解释为 False。一般来说,我们必须小心解读 NaN。它可能表示 False,或者像大多数情况一样,仅表示数据缺失。如果是这种情况,它不应该被转换为 False

  1. 现在,让我们将死亡的个人数据与所涉及的疫苗类型进行关联:

    dead = vdata[vdata.is_dead]
    vax = pd.read_csv("2021VAERSVAX.csv.gz", encoding="iso-8859-1").set_index("VAERS_ID")
    vax.groupby("VAX_TYPE").size().sort_values()
    vax19 = vax[vax.VAX_TYPE == "COVID19"]
    vax19_dead = dead.join(vax19)
    

获取仅包含死亡数据的 DataFrame 后,我们需要读取包含疫苗信息的数据。首先,我们需要进行一些关于疫苗类型及其不良反应的探索性分析。以下是简化后的输出:

           …
HPV9         1506
FLU4         3342
UNK          7941
VARZOS      11034
COVID19    648723

之后,我们必须选择与 COVID 相关的疫苗,并将其与个人数据进行合并。

  1. 最后,让我们看看前 10 个在死亡数量上过度代表的 COVID 疫苗批次,以及每个批次影响的美国州数:

    baddies = vax19_dead.groupby("VAX_LOT").size().sort_values(ascending=False)
    for I, (lot, cnt) in enumerate(baddies.items()):
        print(lot, cnt, len(vax19_dead[vax19_dead.VAX_LOT == lot].groupby""STAT"")))
        if i == 10:
            break
    

输出结果如下:

Unknown 254 34
EN6201 120 30
EN5318 102 26
EN6200 101 22
EN6198 90 23
039K20A 89 13
EL3248 87 17
EL9261 86 21
EM9810 84 21
EL9269 76 18
EN6202 75 18

本节到此结束!

还有更多内容...

关于疫苗和批次的前述数据并不完全正确;我们将在下一个食谱中讨论一些数据分析中的陷阱。

引入 Matplotlib 用于图表生成 的食谱中,我们将介绍 Matplotlib,一个为 pandas 绘图提供后端支持的图表库。它是 Python 数据分析生态系统的一个基础组件。

参见

以下是一些可能有用的额外信息:

处理连接 pandas DataFrame 时的陷阱

上一个食谱是对 pandas 的快速介绍,涵盖了我们在本书中将使用的大部分功能。尽管关于 pandas 的详细讨论需要一本完整的书,但在本食谱(以及下一个食谱)中,我们将讨论一些对数据分析有影响的主题,这些主题在文献中很少讨论,但却非常重要。

在这个食谱中,我们将讨论通过连接(joins)关联 DataFrame 时的一些陷阱:事实证明,许多数据分析错误是由于不小心连接数据所引入的。我们将在这里介绍一些减少此类问题的技巧。

准备工作

我们将使用与上一食谱相同的数据,但会稍微打乱它,以便讨论典型的数据分析陷阱。我们将再次将主要的不良事件表与疫苗表连接,但会从每个表中随机采样 90% 的数据。这模拟了例如你只有不完整信息的场景。这是很多情况下,表之间的连接结果并不直观明显的一个例子。

使用以下代码通过随机采样 90% 的数据来准备我们的文件:

vdata = pd.read_csv("2021VAERSDATA.csv.gz", encoding="iso-8859-1")
vdata.sample(frac=0.9).to_csv("vdata_sample.csv.gz", index=False)
vax = pd.read_csv("2021VAERSVAX.csv.gz", encoding="iso-8859-1")
vax.sample(frac=0.9).to_csv("vax_sample.csv.gz", index=False)

由于此代码涉及随机采样,因此你将得到与此处报告的结果不同的结果。如果你想得到相同的结果,我已经提供了我在 Chapter02 目录中使用的文件。此食谱的代码可以在 Chapter02/Pandas_Join.py 中找到。

如何操作...

请按照以下步骤操作:

  1. 让我们先对个体数据和疫苗数据表做一个内连接:

    vdata = pd.read_csv("vdata_sample.csv.gz")
    vax = pd.read_csv("vax_sample.csv.gz")
    vdata_with_vax = vdata.join(
        vax.set_index("VAERS_ID"),
        on="VAERS_ID",
        how="inner")
    len(vdata), len(vax), len(vdata_with_vax)
    

这个代码的 len 输出结果是:个体数据为 589,487,疫苗数据为 620,361,连接结果为 558,220。这表明一些个体数据和疫苗数据没有被捕获。

  1. 让我们通过以下连接查找未被捕获的数据:

    lost_vdata = vdata.loc[~vdata.index.isin(vdata_with_vax.index)]
    lost_vdata
    lost_vax = vax[~vax["VAERS_ID"].isin(vdata.index)]
    lost_vax
    

你会看到 56,524 行个体数据没有被连接,并且有 62,141 行疫苗数据。

  1. 还有其他方式可以连接数据。默认的方法是执行左外连接:

    vdata_with_vax_left = vdata.join(
        vax.set_index("VAERS_ID"),
        on="VAERS_ID")
    vdata_with_vax_left.groupby("VAERS_ID").size().sort_values()
    

左外连接确保左表中的所有行始终被表示。如果右表没有匹配的行,则所有右侧的列将被填充为 None 值。

警告

有一个警告需要小心。请记住,左表 - vdata - 每个 VAERS_ID 都有一条记录。当你进行左连接时,可能会遇到左侧数据被重复多次的情况。例如,我们之前做的 groupby 操作显示,VAERS_ID 为 962303 的数据有 11 条记录。这是正确的,但也不罕见的是,很多人错误地期望左侧的每一行在输出中仍然是单独一行。这是因为左连接会返回一个或多个左侧条目,而上述的内连接返回的是 0 或 1 条记录,而有时我们希望每个左侧的行都精确对应一条记录。务必始终测试输出结果,确保记录的数量符合预期。

  1. 也有右连接。让我们将 COVID 疫苗数据(左表)与死亡事件数据(右表)做右连接:

    dead = vdata[vdata.DIED == "Y"]
    vax19 = vax[vax.VAX_TYPE == "COVID19"]
    vax19_dead = vax19.join(dead.set_index("VAERS_ID"), on="VAERS_ID", how="right")
    len(vax19), len(dead), len(vax19_dead)
    len(vax19_dead[vax19_dead.VAERS_ID.duplicated()])
    len(vax19_dead) - len(dead)
    

如你所料,右连接将确保右表中的所有行都会被表示出来。因此,我们最终得到了 583,817 个 COVID 记录,7,670 个死亡记录,以及一个 8,624 条记录的右连接。

我们还检查了连接表中的重复条目数量,结果是 954。如果我们从连接表中减去死表的长度,结果也是 954。做连接时,确保进行这样的检查。

  1. 最后,我们将重新审视有问题的 COVID 批次计算,因为我们现在知道我们可能在过度计算批次:

    vax19_dead["STATE"] = vax19_dead["STATE"].str.upper()
    dead_lot = vax19_dead[["VAERS_ID", "VAX_LOT", "STATE"]].set_index(["VAERS_ID", "VAX_LOT"])
    dead_lot_clean = dead_lot[~dead_lot.index.duplicated()]
    dead_lot_clean = dead_lot_clean.reset_index()
    dead_lot_clean[dead_lot_clean.VAERS_ID.isna()]
    baddies = dead_lot_clean.groupby("VAX_LOT").size().sort_values(ascending=False)
    for i, (lot, cnt) in enumerate(baddies.items()):
        print(lot, cnt, len(dead_lot_clean[dead_lot_clean.VAX_LOT == lot].groupby("STATE")))
        if i == 10:
            break
    

注意到我们在这里使用的策略确保了没有重复项:首先,我们限制了将要使用的列的数量,然后移除重复的索引和空的VAERS_ID。这样就能确保VAERS_IDVAX_LOT的组合不重复,并且不会有没有 ID 关联的批次。

还有更多...

除了左连接、内连接和右连接外,还有其他类型的连接。最值得注意的是外连接,它确保两个表中的所有条目都有表示。

确保你对连接操作有测试和断言:一个非常常见的 bug 是对连接行为的预期错误。你还应该确保在连接的列上没有空值,因为空值可能会产生大量多余的元组。

减少 pandas DataFrame 的内存使用

当你处理大量信息时——例如在分析全基因组测序数据时——内存使用可能会成为分析的限制因素。事实证明,天真的 pandas 在内存方面并不是很高效,我们可以大幅减少它的内存消耗。

在这个方案中,我们将重新审视我们的 VAERS 数据,并探讨几种减少 pandas 内存使用的方法。这些变化的影响可能是巨大的:在许多情况下,减少内存消耗可能意味着能否使用 pandas,或者需要采用更复杂的替代方法,如 Dask 或 Spark。

准备就绪

我们将使用第一个方案中的数据。如果你已经运行过它,你就可以开始了;如果没有,请按照其中讨论的步骤操作。你可以在Chapter02/Pandas_Memory.py找到这段代码。

如何操作...

按照这些步骤操作:

  1. 首先,让我们加载数据并检查 DataFrame 的大小:

    import numpy as np
    import pandas as pd
    vdata = pd.read_csv("2021VAERSDATA.csv.gz", encoding="iso-8859-1")
    vdata.info(memory_usage="deep")
    

下面是输出的简化版本:

RangeIndex: 654986 entries, 0 to 654985
Data columns (total 35 columns):
#   Column        Non-Null Count   Dtype  
---  ------        --------------   -----  
0   VAERS_ID      654986 non-null  int64  
2   STATE         572236 non-null  object 
3   AGE_YRS       583424 non-null  float64
6   SEX           654986 non-null  object 
8   SYMPTOM_TEXT  654828 non-null  object 
9   DIED          8536 non-null    object 
31  BIRTH_DEFECT  383 non-null     object 
34  ALLERGIES     330630 non-null  object 
dtypes: float64(5), int64(2), object(28)
memory usage: 1.3 GB

在这里,我们有关于行数、每行类型和非空值的相关信息。最后,我们可以看到,DataFrame 需要高达 1.3 GB 的内存。

  1. 我们还可以检查每一列的大小:

    for name in vdata.columns:
        col_bytes = vdata[name].memory_usage(index=False, deep=True)
        col_type = vdata[name].dtype
        print(
            name,
            col_type, col_bytes // (1024 ** 2))
    

下面是输出的简化版本:

VAERS_ID int64 4
STATE object 34
AGE_YRS float64 4
SEX object 36
RPT_DATE object 20
SYMPTOM_TEXT object 442
DIED object 20
ALLERGIES object 34

SYMPTOM_TEXT占用了 442 MB,相当于我们整个表的 1/3。

  1. 现在,让我们看看DIED这一列。我们能找到更高效的表示方式吗?

    vdata.DIED.memory_usage(index=False, deep=True)
    vdata.DIED.fillna(False).astype(bool).memory_usage(index=False, deep=True)
    

原始列占用了 21,181,488 字节,而我们的压缩表示仅占用 656,986 字节。也就是说减少了 32 倍!

  1. 那么STATE这一列呢?我们能做得更好吗?

    vdata["STATE"] = vdata.STATE.str.upper()
    states = list(vdata["STATE"].unique())
    vdata["encoded_state"] = vdata.STATE.apply(lambda state: states.index(state))
    vdata["encoded_state"] = vdata["encoded_state"].astype(np.uint8)
    vdata["STATE"].memory_usage(index=False, deep=True)
    vdata["encoded_state"].memory_usage(index=False, deep=True)
    

在这里,我们将 STATE 列(文本类型)转换为 encoded_state(数字类型)。这个数字是州名在州列表中的位置。我们使用这个数字来查找州的列表。原始列大约占用 36 MB,而编码后的列只占用 0.6 MB。

作为这种方法的替代方案,您可以查看 pandas 中的分类变量。我更喜欢使用它们,因为它们有更广泛的应用。

  1. 我们可以在 加载 数据时应用大多数这些优化,因此让我们为此做好准备。但现在,我们遇到了一个先有鸡还是先有蛋的问题:为了能够了解州表的内容,我们必须进行第一次遍历,获取州列表,如下所示:

    states = list(pd.read_csv(
        "vdata_sample.csv.gz",
        converters={
           "STATE": lambda state: state.upper()
        },
        usecols=["STATE"]
    )["STATE"].unique())
    

我们有一个转换器,简单地返回州的全大写版本。我们只返回 STATE 列,以节省内存和处理时间。最后,我们从 DataFrame 中获取 STATE 列(该列只有一个字段)。

  1. 最终的优化是 加载数据。假设我们不需要 SYMPTOM_TEXT —— 这大约占数据的三分之一。在这种情况下,我们可以跳过它。以下是最终版本:

    vdata = pd.read_csv(
        "vdata_sample.csv.gz",
        index_col="VAERS_ID",
        converters={
           "DIED": lambda died: died == "Y",
           "STATE": lambda state: states.index(state.upper())
        },
        usecols=lambda name: name != "SYMPTOM_TEXT"
    )
    vdata["STATE"] = vdata["STATE"].astype(np.uint8)
    vdata.info(memory_usage="deep") 
    

我们现在的内存占用为 714 MB,稍微超过原始数据的一半。通过将我们对 STATEDIED 列使用的方法应用于所有其他列,这个数字还可以大大减少。

另请参见

以下是一些可能有用的额外信息:

  • 如果您愿意使用支持库来帮助 Python 处理,请查看下一个关于 Apache Arrow 的食谱,它将帮助您在更多内存效率上节省额外的内存。

  • 如果最终得到的 DataFrame 占用了比单台机器可用内存更多的内存,那么您必须提高处理能力并使用分块处理——我们在 Pandas 上下文中不会涉及——或者使用可以自动处理大数据的工具。Dask(我们将在 第十一章 “*使用 Dask 和 Zarr 进行并行处理” 中讨论)允许您使用类似 pandas 的接口处理超大内存数据集。

使用 Apache Arrow 加速 pandas 处理

当处理大量数据时,例如全基因组测序,pandas 的速度较慢且占用内存较大。Apache Arrow 提供了几种 pandas 操作的更快且更节省内存的实现,并且可以与 pandas 进行互操作。

Apache Arrow 是由 pandas 的创始人 Wes McKinney 共同创立的一个项目,它有多个目标,包括以与语言无关的方式处理表格数据,这样可以实现语言间的互操作性,同时提供高效的内存和计算实现。在这里,我们将只关注第二部分:提高大数据处理的效率。我们将与 pandas 一起以集成的方式实现这一点。

在这里,我们将再次使用 VAERS 数据,并展示如何使用 Apache Arrow 加速 pandas 数据加载并减少内存消耗。

准备工作

再次,我们将使用第一个食谱中的数据。确保你已经按照 准备工作 部分中解释的方式下载并准备好它,代码可以在 Chapter02/Arrow.py 中找到。

如何做到...

按照以下步骤进行:

  1. 让我们开始使用 pandas 和 Arrow 加载数据:

    import gzip
    import pandas as pd
    from pyarrow import csv
    import pyarrow.compute as pc 
    vdata_pd = pd.read_csv("2021VAERSDATA.csv.gz", encoding="iso-8859-1")
    columns = list(vdata_pd.columns)
    vdata_pd.info(memory_usage="deep") 
    vdata_arrow = csv.read_csv("2021VAERSDATA.csv.gz")
    tot_bytes = sum([
        vdata_arrow[name].nbytes
        for name in vdata_arrow.column_names])
    print(f"Total {tot_bytes // (1024 ** 2)} MB")
    

pandas 需要 1.3 GB,而 Arrow 只需 614 MB:不到一半的内存。对于像这样的超大文件,这可能意味着能否将数据加载到内存中进行处理,或者需要寻找其他解决方案,比如 Dask。虽然 Arrow 中某些函数与 pandas 的名称相似(例如,read_csv),但这并不是最常见的情况。例如,注意我们计算 DataFrame 总大小的方法:通过获取每一列的大小并求和,这与 pandas 的方法不同。

  1. 让我们并排比较推断出的类型:

    for name in vdata_arrow.column_names:
        arr_bytes = vdata_arrow[name].nbytes
        arr_type = vdata_arrow[name].type
        pd_bytes = vdata_pd[name].memory_usage(index=False, deep=True)
        pd_type = vdata_pd[name].dtype
        print(
            name,
            arr_type, arr_bytes // (1024 ** 2),
            pd_type, pd_bytes // (1024 ** 2),)
    

这里是输出的简化版本:

VAERS_ID int64 4 int64 4
RECVDATE string 8 object 41
STATE string 3 object 34
CAGE_YR int64 5 float64 4
SEX string 3 object 36
RPT_DATE string 2 object 20
DIED string 2 object 20
L_THREAT string 2 object 20
ER_VISIT string 2 object 19
HOSPITAL string 2 object 20
HOSPDAYS int64 5 float64 4

如你所见,Arrow 在类型推断方面通常更为具体,这也是其内存使用显著更低的主要原因之一。

  1. 现在,让我们做一个时间性能比较:

    %timeit pd.read_csv("2021VAERSDATA.csv.gz", encoding="iso-8859-1")
    %timeit csv.read_csv("2021VAERSDATA.csv.gz")
    

在我的计算机上,结果如下:

7.36 s ± 201 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
2.28 s ± 70.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

Arrow 的实现速度是三倍。由于这取决于硬件,因此你电脑上的结果可能有所不同。

  1. 让我们在不加载 SYMPTOM_TEXT 列的情况下重复内存占用比较。这是一个更公平的比较,因为大多数数值数据集通常没有非常大的文本列:

    vdata_pd = pd.read_csv("2021VAERSDATA.csv.gz", encoding="iso-8859-1", usecols=lambda x: x != "SYMPTOM_TEXT")
    vdata_pd.info(memory_usage="deep")
    columns.remove("SYMPTOM_TEXT")
    vdata_arrow = csv.read_csv(
        "2021VAERSDATA.csv.gz",
         convert_options=csv.ConvertOptions(include_columns=columns))
    vdata_arrow.nbytes
    

pandas 需要 847 MB,而 Arrow 只需 205 MB:少了四倍。

  1. 我们的目标是使用 Arrow 将数据加载到 pandas 中。为此,我们需要转换数据结构:

    vdata = vdata_arrow.to_pandas()
    vdata.info(memory_usage="deep")
    

这里有两点非常重要:Arrow 创建的 pandas 表示只用了 1 GB,而 pandas 从其本地的 read_csv 创建的表示则需要 1.3 GB。这意味着即使你使用 pandas 处理数据,Arrow 也可以首先创建一个更紧凑的表示。

上述代码存在一个内存消耗问题:当转换器运行时,它将需要内存来存储 pandasArrow 的两个表示,从而违背了使用更少内存的初衷。Arrow 可以在创建 pandas 版本的同时自我销毁其表示,从而解决这个问题。相关代码行是 vdata = vdata_arrow.to_pandas(self_destruct=True)

还有更多...

如果你有一个非常大的 DataFrame,甚至在 Arrow 加载后也无法由 pandas 处理,那么或许 Arrow 可以完成所有处理,因为它也有计算引擎。尽管如此,Arrow 的计算引擎在写作时,功能上远不如 pandas 完善。记住,Arrow 还有许多其他功能,例如语言互操作性,但我们在本书中不会使用到这些。

理解 NumPy 作为 Python 数据科学和生物信息学的引擎

即使你没有显式使用 NumPy,大多数分析都将使用 NumPy。NumPy 是一个数组操作库,它是 pandas、Matplotlib、Biopython、scikit-learn 等许多库背后的基础。虽然你在生物信息学工作中可能不需要直接使用 NumPy,但你应该了解它的存在,因为它几乎支持了你所做的一切,即使是通过其他库间接使用。

在这个例子中,我们将使用 VAERS 数据演示 NumPy 如何在我们使用的许多核心库中发挥作用。这是一个非常简要的 NumPy 入门介绍,目的是让你了解它的存在,并知道它几乎在所有东西背后。我们的示例将从五个美国州中提取不良反应案例的数量,并按年龄分组:0 至 19 岁、20 至 39 岁,直到 100 至 119 岁。

准备工作

再次提醒,我们将使用第一个例子中的数据,因此请确保数据可用。相关的代码可以在 Chapter02/NumPy.py 找到。

如何实现…

按照以下步骤操作:

  1. 我们首先通过 pandas 加载数据,并将数据减少到仅与前五个美国州相关:

    import numpy as np
    import pandas as pd
    import matplotlib.pyplot as plt
    vdata = pd.read_csv(
        "2021VAERSDATA.csv.gz", encoding="iso-8859-1")
    vdata["STATE"] = vdata["STATE"].str.upper()
    top_states = pd.DataFrame({
        "size": vdata.groupby("STATE").size().sort_values(ascending=False).head(5)}).reset_index()
    top_states["rank"] = top_states.index
    top_states = top_states.set_index("STATE")
    top_vdata = vdata[vdata["STATE"].isin(top_states.index)]
    top_vdata["state_code"] = top_vdata["STATE"].apply(
        lambda state: top_states["rank"].at[state]
    ).astype(np.uint8)
    top_vdata = top_vdata[top_vdata["AGE_YRS"].notna()]
    top_vdata.loc[:,"AGE_YRS"] = top_vdata["AGE_YRS"].astype(int)
    top_states
    

前五个州如下。这个排名将在后续用来构建 NumPy 矩阵:

图 2.2 – 美国具有最大不良反应数量的州

图 2.2 – 美国具有最大不良反应数量的州

  1. 现在,让我们提取包含年龄和州数据的两个 NumPy 数组:

    age_state = top_vdata[["state_code", "AGE_YRS"]]
    age_state["state_code"]
    state_code_arr = age_state["state_code"].values
    type(state_code_arr), state_code_arr.shape, state_code_arr.dtype
    age_arr = age_state["AGE_YRS"].values
    type(age_arr), age_arr.shape, age_arr.dtype
    

请注意,pandas 背后的数据是 NumPy 数据(对于 Series,values 调用返回的是 NumPy 类型)。此外,你可能还记得 pandas 有 .shape.dtype 等属性:这些属性的设计灵感来源于 NumPy,且行为相同。

  1. 现在,让我们从头开始创建一个 NumPy 矩阵(一个二维数组),其中每一行代表一个州,每一列代表一个年龄组:

    age_state_mat = np.zeros((5,6), dtype=np.uint64)
    for row in age_state.itertuples():
        age_state_mat[row.state_code, row.AGE_YRS//20] += 1
    age_state_mat
    

这个数组有五行——每行代表一个州——和六列——每列代表一个年龄组。数组中的所有单元格必须具有相同的类型。

我们用零来初始化数组。虽然初始化数组有很多方法,但如果你的数组非常大,初始化可能会花费很长时间。有时,根据你的任务,数组一开始为空(意味着它被初始化为随机垃圾)也是可以接受的。在这种情况下,使用 np.empty 会快得多。我们在这里使用 pandas 的迭代:从 pandas 的角度来看,这不是最好的做法,但我们希望让 NumPy 的部分非常明确。

  1. 我们可以非常轻松地提取单一的行——在我们这个例子中,是某个州的数据。对列同样适用。我们来看看加利福尼亚州的数据,然后是 0-19 岁的年龄组:

    cal = age_state_mat[0,:]
    kids = age_state_mat[:,0]
    

注意提取行或列的语法。鉴于 pandas 复制了 NumPy 的语法,且我们在之前的例子中也遇到过,它应该对你来说很熟悉。

  1. 现在,让我们计算一个新的矩阵,矩阵中的每个元素表示每个年龄组的案例比例:

    def compute_frac(arr_1d):
        return arr_1d / arr_1d.sum()
    frac_age_stat_mat = np.apply_along_axis(compute_frac, 1, age_state_mat)
    

最后一行对所有行应用了 compute_frac 函数。compute_frac 接受一行数据并返回一个新行,其中所有元素都被总和除以。

  1. 现在,让我们创建一个新的矩阵,表示百分比而不是比例——这样看起来更清晰:

    perc_age_stat_mat = frac_age_stat_mat * 100
    perc_age_stat_mat = perc_age_stat_mat.astype(np.uint8)
    perc_age_stat_mat
    

第一行代码仅仅是将 2D 数组的所有元素乘以 100。Matplotlib 足够聪明,能够处理不同的数组结构。只要传递给它任何维度的数组,这行代码都能正常工作并按预期进行操作。

这是结果:

图 2.3 – 一个矩阵,表示美国五个病例最多的州中疫苗不良反应的分布

图 2.3 – 一个矩阵,表示美国五个病例最多的州中疫苗不良反应的分布

  1. 最后,让我们使用 Matplotlib 创建该矩阵的图形表示:

    fig = plt.figure()
    ax = fig.add_subplot()
    ax.matshow(perc_age_stat_mat, cmap=plt.get_cmap("Greys"))
    ax.set_yticks(range(5))
    ax.set_yticklabels(top_states.index)
    ax.set_xticks(range(6))
    ax.set_xticklabels(["0-19", "20-39", "40-59", "60-79", "80-99", "100-119"])
    fig.savefig("matrix.png")
    

不要过多纠结于 Matplotlib 的代码——我们将在下一个示例中讨论它。这里的关键点是,你可以将 NumPy 数据结构传递给 Matplotlib。Matplotlib 就像 pandas 一样,是基于 NumPy 的。

另见

以下是一些可能有用的额外信息:

  • NumPy 有许多功能超出了我们在这里讨论的范围。市面上有许多书籍和教程可以帮助学习这些功能。官方文档是一个很好的起点:numpy.org/doc/stable/

  • NumPy 有许多重要的功能值得探索,但其中最重要的可能是广播:NumPy 能够处理不同结构的数组,并正确地进行操作。详细信息请参见 numpy.org/doc/stable/user/theory.broadcasting.xhtml

介绍用于图表生成的 Matplotlib

Matplotlib 是最常用的 Python 图表生成库。虽然也有一些更现代的替代库,如Bokeh,它是以 web 为中心的,但 Matplotlib 的优势不仅在于它是最广泛可用且文档最为丰富的图表库,还因为在计算生物学领域,我们需要一个既适用于 web 又适用于纸质文档的图表库。这是因为我们许多图表将会提交给科学期刊,这些期刊同样关注这两种格式。Matplotlib 能够为我们处理这一需求。

本示例中的许多例子也可以直接使用 pandas(间接使用 Matplotlib)完成,但这里的重点是练习使用 Matplotlib。

我们将再次使用 VAERS 数据来绘制有关 DataFrame 元数据的信息,并总结流行病学数据。

准备工作

再次,我们将使用第一个示例中的数据。代码可以在 Chapter02/Matplotlib.py 中找到。

如何做到…

按照以下步骤操作:

  1. 我们要做的第一件事是绘制每列空值的比例:

    import numpy as np
    import pandas as pd
    import matplotlib as mpl
    import matplotlib.pyplot as plt
    vdata = pd.read_csv(
        "2021VAERSDATA.csv.gz", encoding="iso-8859-1",
        usecols=lambda name: name != "SYMPTOM_TEXT")
    num_rows = len(vdata)
    perc_nan = {}
    for col_name in vdata.columns:
        num_nans = len(vdata[col_name][vdata[col_name].isna()])
        perc_nan[col_name] = 100 * num_nans / num_rows
    labels = perc_nan.keys()
    bar_values = list(perc_nan.values())
    x_positions = np.arange(len(labels))
    

labels 是我们正在分析的列名,bar_values 是空值的比例,x_positions 是接下来要绘制的条形图上条形的位置。

  1. 以下是第一个版本的条形图代码:

    fig = plt.figure()
    fig.suptitle("Fraction of empty values per column")
    ax = fig.add_subplot()
    ax.bar(x_positions, bar_values)
    ax.set_ylabel("Percent of empty values")
    ax.set_ylabel("Column")
    ax.set_xticks(x_positions)
    ax.set_xticklabels(labels)
    ax.legend()
    fig.savefig("naive_chart.png")
    

我们首先创建一个带有标题的图形对象。该图形将包含一个子图,用于显示条形图。我们还设置了几个标签,并且仅使用了默认设置。以下是令人沮丧的结果:

图 2.4 – 我们的第一次图表尝试,使用默认设置

图 2.4 – 我们的第一次图表尝试,使用默认设置

  1. 当然,我们可以做得更好。让我们对图表进行更大幅度的格式化:

    fig = plt.figure(figsize=(16, 9), tight_layout=True, dpi=600)
    fig.suptitle("Fraction of empty values per column", fontsize="48")
    ax = fig.add_subplot()
    b1 = ax.bar(x_positions, bar_values)
    ax.set_ylabel("Percent of empty values", fontsize="xx-large")
    ax.set_xticks(x_positions)
    ax.set_xticklabels(labels, rotation=45, ha="right")
    ax.set_ylim(0, 100)
    ax.set_xlim(-0.5, len(labels))
    for i, x in enumerate(x_positions):
        ax.text(
            x, 2, "%.1f" % bar_values[i], rotation=90,
            va="bottom", ha="center",
            backgroundcolor="white")
    fig.text(0.2, 0.01, "Column", fontsize="xx-large")
    fig.savefig("cleaner_chart.png")
    

我们做的第一件事是为 Matplotlib 设置一个更大的图形,以提供更紧凑的布局。我们将 x 轴的刻度标签旋转 45 度,使其更合适地显示。我们还在条形上标注了数值。最后,我们没有使用标准的 x 轴标签,因为它会遮挡住刻度标签。相反,我们明确写出了文本。请注意,图形的坐标系与子图的坐标系可能完全不同——例如,比较 ax.textfig.text 的坐标。以下是结果:

图 2.5 – 我们的第二次图表尝试,已考虑布局问题

图 2.5 – 我们的第二次图表尝试,已考虑布局问题

  1. 现在,我们将根据一个图形上的四个子图来对数据进行一些汇总分析。我们将展示与死亡相关的疫苗、接种与死亡之间的天数、随时间变化的死亡情况以及十大州的死亡者性别:

    dead = vdata[vdata.DIED == "Y"]
    vax = pd.read_csv("2021VAERSVAX.csv.gz", encoding="iso-8859-1").set_index("VAERS_ID")
    vax_dead = dead.join(vax, on="VAERS_ID", how="inner")
    dead_counts = vax_dead["VAX_TYPE"].value_counts()
    large_values = dead_counts[dead_counts >= 10]
    other_sum = dead_counts[dead_counts < 10].sum()
    large_values = large_values.append(pd.Series({"OTHER": other_sum}))
    distance_df = vax_dead[vax_dead.DATEDIED.notna() & vax_dead.VAX_DATE.notna()]
    distance_df["DATEDIED"] = pd.to_datetime(distance_df["DATEDIED"])
    distance_df["VAX_DATE"] = pd.to_datetime(distance_df["VAX_DATE"])
    distance_df = distance_df[distance_df.DATEDIED >= "2021"]
    distance_df = distance_df[distance_df.VAX_DATE >= "2021"]
    distance_df = distance_df[distance_df.DATEDIED >= distance_df.VAX_DATE]
    time_distances = distance_df["DATEDIED"] - distance_df["VAX_DATE"]
    time_distances_d = time_distances.astype(int) / (10**9 * 60 * 60 * 24)
    date_died = pd.to_datetime(vax_dead[vax_dead.DATEDIED.notna()]["DATEDIED"])
    date_died = date_died[date_died >= "2021"]
    date_died_counts = date_died.value_counts().sort_index()
    cum_deaths = date_died_counts.cumsum()
    state_dead = vax_dead[vax_dead["STATE"].notna()][["STATE", "SEX"]]
    top_states = sorted(state_dead["STATE"].value_counts().head(10).index)
    top_state_dead = state_dead[state_dead["STATE"].isin(top_states)].groupby(["STATE", "SEX"]).size()#.reset_index()
    top_state_dead.loc["MN", "U"] = 0  # XXXX
    top_state_dead = top_state_dead.sort_index().reset_index()
    top_state_females = top_state_dead[top_state_dead.SEX == "F"][0]
    top_state_males = top_state_dead[top_state_dead.SEX == "M"][0]
    top_state_unk = top_state_dead[top_state_dead.SEX == "U"][0]
    

上述代码完全基于 pandas,并为绘图活动做了准备。

  1. 以下代码同时绘制所有信息。我们将有四个子图,按 2x2 格式排列:

    fig, ((vax_cnt, time_dist), (death_time, state_reps)) = plt.subplots(
        2, 2,
        figsize=(16, 9), tight_layout=True)
    vax_cnt.set_title("Vaccines involved in deaths")
    wedges, texts = vax_cnt.pie(large_values)
    vax_cnt.legend(wedges, large_values.index, loc="lower left")
    time_dist.hist(time_distances_d, bins=50)
    time_dist.set_title("Days between vaccine administration and death")
    time_dist.set_xlabel("Days")
    time_dist.set_ylabel("Observations")
    death_time.plot(date_died_counts.index, date_died_counts, ".")
    death_time.set_title("Deaths over time")
    death_time.set_ylabel("Daily deaths")
    death_time.set_xlabel("Date")
    tw = death_time.twinx()
    tw.plot(cum_deaths.index, cum_deaths)
    tw.set_ylabel("Cummulative deaths")
    state_reps.set_title("Deaths per state stratified by sex") state_reps.bar(top_states, top_state_females, label="Females")
    state_reps.bar(top_states, top_state_males, label="Males", bottom=top_state_females)
    state_reps.bar(top_states, top_state_unk, label="Unknown",
                   bottom=top_state_females.values + top_state_males.values)
    state_reps.legend()
    state_reps.set_xlabel("State")
    state_reps.set_ylabel("Deaths")
    fig.savefig("summary.png")
    

我们首先创建一个 2x2 的子图图形。subplots 函数返回图形对象及四个坐标轴对象,我们可以使用这些坐标轴对象来创建我们的图表。请注意,图例位于饼图中,我们在时间距离图上使用了双坐标轴,并且在每个州的死亡率图上计算了堆积条形图。以下是结果:

图 2.6 – 汇总疫苗数据的四个合并图表

图 2.6 – 汇总疫苗数据的四个合并图表

还有更多...

Matplotlib 有两个接口可供使用——一个较旧的接口,设计上类似于 MATLAB,另一个是功能更强大的 matplotlib.pyplot 模块。为了增加混淆,面向对象接口的入口点就在这个模块中——即 matplotlib.pyplot.figurematplotlib.pyplot.subplots

另见

以下是一些可能有用的额外信息:

  • Matplotlib 的文档真的非常出色。例如,它提供了一个包含每个示例代码链接的可视化样本画廊。你可以在matplotlib.org/stable/gallery/index.xhtml找到这个画廊。API 文档通常非常完整。

  • 改善 Matplotlib 图表外观的另一种方法是使用 Seaborn 库。Seaborn 的主要目的是添加统计可视化工具,但作为副作用,它在导入时会将 Matplotlib 的默认设置更改为更易接受的样式。我们将在本书中始终使用 Seaborn;请查看下一章中提供的图表。

第四章:下一代测序

下一代测序NGS)是本世纪生命科学领域的基础性技术发展之一。全基因组测序WGS)、限制酶切位点关联 DNA 测序RAD-Seq)、核糖核酸测序RNA-Seq)、染色质免疫沉淀测序ChIP-Seq)以及其他几种技术已被广泛应用于研究重要的生物学问题。这些技术也被称为高通量测序技术,且理由充分:它们产生大量需要处理的数据。NGS 是计算生物学成为大数据学科的主要原因。最重要的是,这是一个需要强大生物信息学技术的领域。

在这里,我们不会讨论每一种单独的 NGS 技术本身(这将需要一本完整的书)。我们将使用现有的 WGS 数据集——千人基因组计划,来说明分析基因组数据所需的最常见步骤。这里提供的步骤可以轻松应用于其他基因组测序方法。其中一些也可以用于转录组分析(例如,RNA-Seq)。这些步骤也与物种无关,因此你可以将它们应用于任何其他已测序物种的数据。来自不同物种的数据处理之间最大的差异与基因组大小、多样性以及参考基因组的质量(如果存在的话)有关。这些因素不会对 NGS 处理的自动化 Python 部分产生太大影响。无论如何,我们将在第五章中讨论不同的基因组,基因组分析

由于这不是一本入门书籍,你需要至少了解FASTAFASTA)、FASTQ、二进制比对映射BAM)和变异调用格式VCF)文件是什么。我还将使用一些基本的基因组术语而不做介绍(例如外显子、非同义突变等)。你需要具备基本的 Python 知识。我们将利用这些知识来介绍 Python 中进行 NGS 分析的基本库。在这里,我们将遵循标准的生物信息学流程。

然而,在我们深入研究来自真实项目的真实数据之前,让我们先熟悉访问现有的基因组数据库和基本的序列处理——在风暴来临之前的简单开始。

如果你通过 Docker 运行内容,可以使用tiagoantao/bioinformatics_ngs镜像。如果你使用的是 Anaconda Python,本章所需的软件将在每个步骤中介绍。

在本章中,我们将涵盖以下步骤:

  • 访问 GenBank 并浏览国家生物技术信息中心NCBI)数据库

  • 执行基本的序列分析

  • 处理现代序列格式

  • 处理比对数据

  • 从 VCF 文件中提取数据

  • 研究基因组可访问性并筛选单核苷酸多态性SNP)数据

  • 使用 HTSeq 处理 NGS 数据

访问 GenBank 并在 NCBI 数据库中浏览

虽然你可能有自己的数据需要分析,但你很可能需要现有的基因组数据集。在这里,我们将探讨如何访问 NCBI 的这些数据库。我们不仅会讨论 GenBank,还会讨论 NCBI 的其他数据库。许多人错误地将整个 NCBI 数据库集称为 GenBank,但 NCBI 还包括核苷酸数据库和其他许多数据库——例如,PubMed。

由于测序分析是一个庞大的主题,而本书面向的是中高级用户,我们不会对这个本质上并不复杂的主题进行非常详尽的讲解。

尽管如此,这也是一个很好的热身练习,为我们在本章末尾看到的更复杂的教程做准备。

准备工作

我们将使用你在第一章中安装的 Biopython,Python 与周围的软件生态。Biopython 为Entrez提供了接口,Entrez是 NCBI 提供的数据检索系统。

这个教程可以在Chapter03/Accessing_Databases.py文件中找到。

小贴士

你将访问 NCBI 的实时应用程序编程接口API)。请注意,系统的性能可能会在一天中有所波动。此外,使用时需要保持“良好的公民行为”。你可以在www.ncbi.nlm.nih.gov/books/NBK25497/#chapter2.Usage_Guidelines_and_Requiremen找到一些使用建议。特别需要注意的是,查询时你需要指定一个电子邮件地址。你应尽量避免在高峰时段(工作日美国东部时间上午 9:00 到下午 5:00 之间)发出大量请求(100 个或更多),并且每秒不超过三次查询(Biopython 会为你处理这个问题)。这不仅是为了遵守规定,而且如果你过度使用 NCBI 的服务器,你可能会被封锁(这也是一个好理由提供真实的电子邮件地址,因为 NCBI 可能会联系你)。

如何操作...

现在,让我们来看看如何从 NCBI 数据库中搜索和获取数据:

  1. 我们将从导入相关模块并配置电子邮件地址开始:

    from Bio import Entrez, SeqIO
    Entrez.email = 'put@your.email.here'
    

我们还将导入用于处理序列的模块。请不要忘记填写正确的电子邮件地址。

  1. 我们现在将尝试在nucleotide数据库中查找Plasmodium falciparum(导致最致命疟疾的寄生虫):

    handle = Entrez.esearch(db='nucleotide', term='CRT[Gene Name] AND "Plasmodium falciparum"[Organism]')
    rec_list = Entrez.read(handle)
    if int(rec_list['RetMax']) < int(rec_list['Count']):
        handle = Entrez.esearch(db='nucleotide', term='CRT[Gene Name] AND "Plasmodium falciparum"[Organism]', retmax=rec_list['Count'])
        rec_list = Entrez.read(handle)
    

我们将搜索 nucleotide 数据库中的基因和生物体(关于搜索字符串的语法,参考 NCBI 网站)。然后,我们将读取返回的结果。请注意,标准搜索会将记录引用的数量限制为 20,因此如果有更多记录,可能需要重复查询并增加最大限制。在我们的例子中,我们将使用 retmax 来覆盖默认限制。Entrez 系统提供了多种复杂的方法来检索大量结果(更多信息请参考 Biopython 或 NCBI Entrez 文档)。尽管现在你已经拥有了所有记录的标识符ID),但你仍然需要正确地检索记录。

  1. 现在,让我们尝试检索所有这些记录。以下查询将从 GenBank 下载所有匹配的核苷酸序列,截至本书编写时,共有 1,374 条。你可能不想每次都这样做:

    id_list = rec_list['IdList']
    hdl = Entrez.efetch(db='nucleotide', id=id_list, rettype='gb')
    

好吧,在这种情况下,继续进行吧。然而,使用这种技术时要小心,因为你将检索大量完整的记录,其中一些记录包含相当大的序列。你有可能下载大量数据(这会对你的机器和 NCBI 服务器都造成压力)。

有几种方法可以解决这个问题。一个方法是进行更严格的查询和/或每次只下载少量记录,当你找到需要的记录时就停止。具体策略取决于你的目标。无论如何,我们将检索 GenBank 格式的记录列表(该格式包含序列以及大量有趣的元数据)。

  1. 让我们读取并解析结果:

    recs = list(SeqIO.parse(hdl, 'gb'))
    

请注意,我们已经将一个迭代器(SeqIO.parse 的结果)转换为列表。这样做的好处是我们可以多次使用结果(例如,多次迭代),而无需在服务器上重复查询。

如果你打算进行多次迭代,这可以节省时间、带宽和服务器使用。缺点是它会为所有记录分配内存。对于非常大的数据集,这种方法不可行;你可能不想像在 第五章 《处理基因组》那样进行全基因组的转换。如果你正在进行交互式计算,你可能更倾向于获取一个列表(以便你可以多次分析和实验),但如果你在开发一个库,迭代器可能是最佳的选择。

  1. 现在我们将集中精力处理单条记录。只有当你使用完全相同的前一个查询时,这种方法才有效:

    for rec in recs:
        if rec.name == 'KM288867':
            break
    print(rec.name)
    print(rec.description)
    

rec 变量现在包含我们感兴趣的记录。rec.description 文件将包含它的人类可读描述。

  1. 现在,让我们提取一些包含 gene 产品和序列中 exon 位置等信息的序列特征:

    for feature in rec.features:
         if feature.type == 'gene':
             print(feature.qualifiers['gene'])
         elif feature.type == 'exon':
             loc = feature.location
             print(loc.start, loc.end, loc.strand)
         else:
             print('not processed:\n%s' % feature)
    

如果feature.type值是gene,我们将打印其名称,它会在qualifiers字典中。我们还将打印所有外显子的位置。外显子和所有特征一样,具有该序列的位置:起始位置、结束位置,以及它们读取的链。虽然所有外显子的起始和结束位置都是ExactPosition,但请注意,Biopython 支持许多其他类型的位置。位置的一种类型是BeforePosition,它指定某个位置点位于某个特定序列位置之前。另一种类型是BetweenPosition,它给出了某个位置的起始/结束区间。还有许多其他位置类型,这些只是一些例子。

坐标将以这样一种方式指定,使得你能够轻松地从带有范围的 Python 数组中检索序列,因此通常开始位置会比记录上的值小 1,结束位置将相等。坐标系统的问题将在未来的教程中再次讨论。

对于其他特征类型,我们只需打印它们。请注意,当你打印特征时,Biopython 会提供一个可读的人类版本。

  1. 现在我们来看一下记录上的注释,这些注释主要是与序列位置无关的元数据:

    for name, value in rec.annotations.items():
        print('%s=%s' % (name, value))
    

请注意,有些值不是字符串;它们可以是数字,甚至是列表(例如,分类注释就是一个列表)。

  1. 最后但同样重要的是,你可以访问一个基础信息——序列:

    print(len(rec.seq))
    

序列对象将是我们下一个教程的主要主题。

还有更多内容...

NCBI 还有许多其他数据库。如果你正在处理 NGS 数据,可能会想查看序列读取档案SRA)数据库(以前称为短序列档案)。SNP 数据库包含 SNP 信息,而蛋白质数据库包含蛋白质序列,等等。Entrez 数据库的完整列表可在本教程的另见部分找到。

另一个你可能已经知道的与 NCBI 相关的数据库是 PubMed,它包含了科学和医学的引用、摘要,甚至是全文。你也可以通过 Biopython 访问它。此外,GenBank 记录通常包含指向 PubMed 的链接。例如,我们可以在之前的记录上执行此操作,如下所示:

from Bio import Medline
refs = rec.annotations['references']
for ref in refs:
    if ref.pubmed_id != '':
        print(ref.pubmed_id)
        handle = Entrez.efetch(db='pubmed', id=[ref.pubmed_id], rettype='medline', retmode='text')
        records = Medline.parse(handle)
        for med_rec in records:
            for k, v in med_rec.items():
                print('%s: %s' % (k, v))

这将获取所有参考注释,检查它们是否具有 PubMed ID,然后访问 PubMed 数据库以检索记录,解析它们,并打印出来。

每条记录的输出是一个 Python 字典。请注意,典型的 GenBank 记录中有许多指向外部数据库的引用。

当然,NCBI 之外还有许多其他生物数据库,如 Ensembl(www.ensembl.org)和加利福尼亚大学圣克鲁兹分校UCSC)基因组生物信息学(genome.ucsc.edu/)。这些数据库在 Python 中的支持程度差异很大。

介绍生物数据库的食谱如果没有至少提到基础本地比对搜索工具BLAST),那就不完整了。BLAST 是一种评估序列相似性的算法。NCBI 提供了一项服务,允许你将目标序列与其数据库进行比对。当然,你也可以使用本地 BLAST 数据库,而不是 NCBI 的服务。Biopython 为此提供了广泛的支持,但由于这是一个入门教程,我仅将你指引至 Biopython 的教程。

另见

这些附加信息也会很有用:

执行基本的序列分析

现在我们将对 DNA 序列进行一些基本分析。我们将处理 FASTA 文件,并进行一些操作,例如反向互补或转录。与前面介绍的食谱一样,我们将使用你在第一章中安装的 Biopython,Python 与周边软件生态。这两道食谱为你提供了执行本章和第五章中所有现代 NGS 分析和基因组处理所需的入门基础。

准备工作

本食谱的代码可以在Chapter03/Basic_Sequence_Processing.py中找到。我们将使用人类Entrez研究接口:

from Bio import Entrez, SeqIO, SeqRecord
Entrez.email = "your@email.here"
hdl = Entrez.efetch(db='nucleotide', id=['NM_002299'], rettype='gb') # Lactase gene
gb_rec = SeqIO.read(hdl, 'gb')

现在我们有了 GenBank 记录,让我们提取基因序列。该记录中包含的内容比这更多,但首先让我们获取基因的精确位置:

for feature in gb_rec.features:
    if feature.type == 'CDS':
        location = feature.location  # Note translation existing
cds = SeqRecord.SeqRecord(gb_rec.seq[location.start:location.end], 'NM_002299', description='LCT CDS only')

我们的示例序列已经包含在 Biopython 序列记录中。

如何操作...

让我们来看一下接下来的步骤:

  1. 由于我们的目标序列已经存储在一个 Biopython 序列对象中,首先让我们把它保存为本地磁盘上的 FASTA 文件:

    from Bio import SeqIO
    w_hdl = open('example.fasta', 'w')
    SeqIO.write([cds], w_hdl, 'fasta')
    w_hdl.close()
    

SeqIO.write函数接受一个序列列表进行写入(在我们这里只是一个序列)。使用这个惯用法时要小心。如果你想写入多个序列(并且你可能会用 NGS 写入数百万个序列),不要使用列表(如前面的代码片段所示),因为这会分配大量内存。应使用迭代器,或者每次写入时只处理序列的子集,并多次调用SeqIO.write函数。

  1. 在大多数情况下,你实际上会把序列保存在磁盘上,因此你会对读取它感兴趣:

    recs = SeqIO.parse('example.fasta', 'fasta')
    for rec in recs:
        seq = rec.seq
        print(rec.description)
        print(seq[:10])
    

在这里,我们关注的是处理单一序列,但 FASTA 文件可以包含多个记录。实现这一点的 Python 惯用法非常简单。要读取 FASTA 文件,你只需使用标准的迭代技巧,如以下代码片段所示。对于我们的示例,前面的代码将打印以下输出:

NM_002299 LCT CDS only
 ATGGAGCTGT

请注意,我们打印了seq[:10]。序列对象可以使用典型的数组切片来获取序列的一部分。

  1. 既然我们现在有了明确无误的 DNA,我们可以按如下方式转录它:

    rna = seq.transcribe()
    print(rna)
    
  2. 最终,我们可以将我们的基因转化为蛋白质:

    prot = seq.translate()
    print(prot)
    

现在,我们已经有了我们基因的氨基酸序列。

还有更多...

关于在 Biopython 中管理序列,还可以说很多,但这些主要是介绍性的内容,你可以在 Biopython 教程中找到。我认为给你们一个序列管理的概述是很重要的,主要是为了完整性。为了支持那些可能在生物信息学其他领域有一些经验,但刚刚开始做序列分析的人,尽管如此,仍然有几个点是你们应该注意的:

  • 当你执行 RNA 翻译以获得你的蛋白质时,请确保使用正确的遗传密码。即使你正在处理“常见”的生物体(如人类),也要记住线粒体的遗传密码是不同的。

  • Biopython 的Seq对象比这里展示的要灵活得多。有一些很好的例子可以参考 Biopython 教程。然而,这个教程足以应付我们需要处理的 FASTQ 文件(请参见下一个教程)。

  • 为了处理与链相关的问题,正如预期的那样,有一些序列函数,比如reverse_complement

  • 我们开始的 GenBank 记录包含了关于序列的大量元数据,所以一定要深入探索。

另请参见

使用现代序列格式

在这里,我们将处理 FASTQ 文件,这是现代测序仪使用的标准格式输出。你将学习如何处理每个碱基的质量分数,并考虑不同测序仪和数据库的输出变化。这是第一个将使用来自人类 1000 基因组计划的真实数据(大数据)的教程。我们将首先简要介绍这个项目。

准备工作

人类 1,000 基因组计划旨在 catalog 世界范围的人类遗传变异,并利用现代测序技术进行全基因组测序(WGS)。这个项目使所有数据公开可用,包括来自测序仪的输出、序列比对、SNP 调用以及许多其他数据。这一名称“1,000 基因组”实际上是一个误称,因为它目前包含了超过 2,500 个样本。这些样本分布在数百个人群中,遍布全球。我们将主要使用四个人群的数据:非洲约鲁巴人YRI),拥有北欧和西欧血统的犹他州居民CEU),东京的日本人JPT),以及北京的汉族人CHB)。之所以选择这些特定的人群,是因为它们是最早来自 HapMap 的样本,HapMap 是一个具有类似目标的旧项目。它们使用基因分型阵列来进一步了解该子集的质量。我们将在第六章种群遗传学”中重新回顾 1,000 基因组和 HapMap 项目。

提示

下一代数据集通常非常大。由于我们将使用真实数据,你下载的某些文件可能会非常大。尽管我已经尽量选择最小的真实示例,但你仍然需要一个良好的网络连接以及相当大的磁盘空间。等待下载可能是你在这个配方中遇到的最大障碍,但数据管理是 NGS 中一个严重的问题。在现实生活中,你需要为数据传输预留时间,分配磁盘空间(这可能会涉及财务成本),并考虑备份策略。NGS 的最常见初始错误是认为这些问题微不足道,但事实并非如此。像将一组 BAM 文件复制到网络,甚至复制到你的计算机上,都会变成一件头疼的事。要做好准备。下载大文件后,至少应检查文件大小是否正确。一些数据库提供消息摘要 5MD5)校验和。你可以通过使用像 md5sum 这样的工具,将这些校验和与下载文件中的校验和进行对比。

下载数据的指令位于笔记本的顶部,正如Chapter03/Working_with_FASTQ.py文件的第一单元格中所指定的。这是一个相当小的文件(27 NA18489)。如果你参考 1,000 基因组计划,你会发现绝大多数的 FASTQ 文件要大得多(大约大两个数量级)。

FASTQ 序列文件的处理将主要使用 Biopython 来完成。

如何操作...

在我们开始编写代码之前,让我们先看一下 FASTQ 文件,你将会看到许多记录,正如以下代码片段所示:

@SRR003258.1 30443AAXX:1:1:1053:1999 length=51
 ACCCCCCCCCACCCCCCCCCCCCCCCCCCCCCCCCCCACACACACCAACAC
 +
 =IIIIIIIII5IIIIIII>IIII+GIIIIIIIIIIIIII(IIIII01&III

第 1 行@开始,后面跟着一个序列 ID 和描述字符串。描述字符串会根据测序仪或数据库源有所不同,但通常是可以自动解析的。

第二行包含测序的 DNA,类似于 FASTA 文件。第三行是一个+符号,有时后面会跟上第一行的描述行。

第四行包含每个在第二行读取的碱基的质量值。每个字母编码一个 Phred 质量分数(en.wikipedia.org/wiki/Phred_quality_score),该分数为每个读取分配一个错误概率。这个编码在不同平台间可能略有不同。请确保在您的特定平台上检查此内容。

让我们看看接下来的步骤:

  1. 让我们打开文件:

    import gzip
    from Bio import SeqIO
    recs = SeqIO.parse(gzip.open('SRR003265.filt.fastq.gz'),'rt', encoding='utf-8'), 'fastq')
    rec = next(recs)
    print(rec.id, rec.description, rec.seq)
    print(rec.letter_annotations)
    

我们将打开一个gzip模块,并指定fastq格式。请注意,这种格式的某些变体会影响 Phred 质量分数的解释。您可能需要指定一个稍有不同的格式。有关所有格式的详细信息,请参考biopython.org/wiki/SeqIO

提示

通常,您应该将 FASTQ 文件存储为压缩格式。这样不仅可以节省大量磁盘空间(因为这些是文本文件),而且可能还可以节省一些处理时间。尽管解压缩是一个较慢的过程,但它仍然可能比从磁盘读取一个更大的(未压缩的)文件更快。

我们将从之前的例子中打印标准字段和质量分数到rec.letter_annotations。只要我们选择正确的解析器,Biopython 会将所有 Phred 编码字母转换为对数分数,我们很快就会使用这些分数。

目前,不要这样做:

recs = list(recs) # do not do it!

尽管这种方法可能适用于某些 FASTA 文件(以及这个非常小的 FASTQ 文件),但如果您这样做,您将分配内存以便将完整文件加载到内存中。对于一个普通的 FASTQ 文件,这是让您的计算机崩溃的最佳方式。一般来说,始终遍历您的文件。如果您需要对文件执行多次操作,您有两个主要选择。第一种选择是一次性执行所有操作。第二种选择是多次打开文件并重复迭代。

  1. 现在,让我们看看核苷酸读取的分布:

    from collections import defaultdict
    recs = SeqIO.parse(gzip.open('SRR003265.filt.fastq.gz', 'rt', encoding='utf-8'), 'fastq')
    cnt = defaultdict(int)
    for rec in recs:
        for letter in rec.seq:
            cnt[letter] += 1
    tot = sum(cnt.values())
    for letter, cnt in cnt.items():
        print('%s: %.2f %d' % (letter, 100\. * cnt / tot, cnt))
    

我们将重新打开文件并使用defaultdict来维护 FASTQ 文件中核苷酸引用的计数。如果您从未使用过这种 Python 标准字典类型,您可能会考虑使用它,因为它消除了初始化字典条目的需要,并为每种类型假设默认值。

注意

这里有一个N调用的剩余数目。这些是测序仪报告的未知碱基。在我们的 FASTQ 文件示例中,我们稍微作弊了一下,因为我们使用了一个过滤后的文件(N调用的比例会非常低)。在从测序仪未过滤的文件中,您会看到更多的N调用。事实上,您可能还会看到关于N调用的空间分布方面更多的信息。

  1. 让我们根据读取位置绘制N的分布:

    import seaborn as sns
    import matplotlib.pyplot as plt
    recs = SeqIO.parse(gzip.open('SRR003265.filt.fastq.gz', 'rt', encoding='utf-8'), 'fastq')
    n_cnt = defaultdict(int)
    for rec in recs:
        for i, letter in enumerate(rec.seq):
            pos = i + 1
            if letter == 'N':
                n_cnt[pos] += 1
    seq_len = max(n_cnt.keys())
    positions = range(1, seq_len + 1)
    fig, ax = plt.subplots(figsize=(16,9))
    ax.plot(positions, [n_cnt[x] for x in positions])
    fig.suptitle('Number of N calls as a function of the distance from the start of the sequencer read')
    ax.set_xlim(1, seq_len)
    ax.set_xlabel('Read distance')
    ax.set_ylabel('Number of N Calls')
    

我们导入seaborn库。尽管目前我们并没有显式使用它,但这个库的优点在于它能使matplotlib绘图看起来更好,因为它调整了默认的matplotlib样式。

然后,我们再次打开文件进行解析(记住,这时你不使用列表,而是再次进行迭代)。我们遍历文件,找出所有指向N的引用位置。接着,我们将N的分布作为序列开始位置的距离函数绘制出来:

图 3.1 – 调用的数量与序列读取起始位置的距离函数

图 3.1 – N调用的数量与序列读取起始位置的距离函数

你会看到,直到位置25,没有错误。这并不是你从典型的测序仪输出中得到的结果。我们的示例文件已经经过过滤,1,000 基因组过滤规则要求在位置25之前不能有N调用。

虽然我们不能在位置25之前研究N在这个数据集中的行为(如果你有一个未过滤的 FASTQ 文件,可以使用这段代码查看N在读取位置的分布),但我们可以看到,在位置25之后,分布远非均匀。在这里有一个重要的教训,那就是未调用的碱基数量是与位置相关的。那么,读取的质量如何呢?

  1. 让我们研究 Phred 分数的分布(即我们读取的质量):

    recs = SeqIO.parse(gzip.open('SRR003265.filt.fastq.gz', 'rt', encoding='utf-8'), 'fastq')
    cnt_qual = defaultdict(int)
    for rec in recs:
        for i, qual in enumerate(rec.letter_annotations['phred_quality']):
            if i < 25:
                continue
            cnt_qual[qual] += 1
    tot = sum(cnt_qual.values())
    for qual, cnt in cnt_qual.items():
        print('%d: %.2f %d' % (qual, 100\. * cnt / tot, cnt))
    

我们将重新打开文件(再次)并初始化一个默认字典。然后,我们获取phred_quality字母注释,但我们忽略从起始位置到24的测序位置碱基对bp)(由于我们的 FASTQ 文件已过滤,如果你有未过滤的文件,可能需要删除此规则)。我们将质量分数添加到默认字典中,最后打印出来。

注意

简单提醒一下,Phred 质量分数是准确调用概率的对数表示。这个概率可以表示为!。因此,Q 为 10 表示 90% 的调用准确率,20 表示 99% 的调用准确率,30 表示 99.9%。对于我们的文件,最大准确率将是 99.99%(40)。在某些情况下,60 的值是可能的(99.9999%的准确率)。

  1. 更有趣的是,我们可以根据读取位置绘制质量分布:

    recs = SeqIO.parse(gzip.open('SRR003265.filt.fastq.gz', 'rt', encoding='utf-8'), 'fastq')
    qual_pos = defaultdict(list)
    for rec in recs:
        for i, qual in enumerate(rec.letter_annotations['phred_quality']):
            if i < 25 or qual == 40:
               continue
            pos = i + 1
            qual_pos[pos].append(qual)
    vps = []
    poses = list(qual_pos.keys())
    poses.sort()
    for pos in poses:
        vps.append(qual_pos[pos])
    fig, ax = plt.subplots(figsize=(16,9))
    sns.boxplot(data=vps, ax=ax)
    ax.set_xticklabels([str(x) for x in range(26, max(qual_pos.keys()) + 1)])
    ax.set_xlabel('Read distance')
    ax.set_ylabel('PHRED score')
    fig.suptitle('Distribution of PHRED scores as a function of read distance')
    

在这种情况下,我们将忽略序列位置为从起始位置25 bp 的两个位置(如果你有未过滤的测序数据,请删除此规则),以及该文件的最大质量分数(40)。不过,在你的情况下,你可以考虑从最大值开始绘制分析。你可能想要检查你测序仪硬件的最大可能值。通常,由于大多数调用可以在最大质量下执行,如果你想了解质量问题的所在,可能会希望删除这些数据。

请注意,我们使用的是seabornboxplot函数;我们之所以使用它,是因为它的输出效果比matplotlib的标准boxplot函数稍好。如果你不想依赖seaborn,可以直接使用matplotlib的内建函数。在这种情况下,你可以调用ax.boxplot(vps),而不是sns.boxplot(data=vps, ax=ax)

正如预期的那样,分布并不均匀,如下图所示:

图 3.2 – Phred 得分与测序读取起始位置距离的关系分布

图 3.2 – Phred 得分与测序读取起始位置距离的关系分布

还有更多内容...

尽管无法讨论来自测序仪文件的所有输出变化,但双端读取值得一提,因为它们很常见并且需要不同的处理方式。在双端测序中,DNA 片段的两端会被测序,并且中间有一个间隙(称为插入)。在这种情况下,将生成两个文件:X_1.FASTQX_2.FASTQ。这两个文件的顺序相同,且包含相同数量的序列。X_1中的第一个序列与X_2中的第一个序列配对,以此类推。关于编程技巧,如果你想保持配对信息,你可以执行如下操作:

f1 = gzip.open('X_1.filt.fastq.gz', 'rt, enconding='utf-8')
f2 = gzip.open('X_2.filt.fastq.gz', 'rt, enconding='utf-8')
recs1 = SeqIO.parse(f1, 'fastq')
recs2 = SeqIO.parse(f2, 'fastq')
cnt = 0
for rec1, rec2 in zip(recs1, recs2):
    cnt +=1
print('Number of pairs: %d' % cnt)

上面的代码按顺序读取所有配对并简单地计算配对的数量。你可能需要做更多的操作,但这展示了一种基于 Python zip函数的语法,它允许你同时迭代两个文件。记得用你的FASTQ前缀替换X

最后,如果你正在测序人类基因组,可能想要使用 Complete Genomics 的测序数据。在这种情况下,请阅读下一个食谱中的更多内容…部分,我们会简要讨论 Complete Genomics 数据。

另见

这里有一些提供更多信息的链接:

处理对齐数据

在您从测序仪获得数据后,通常会使用像bwa这样的工具将您的序列与参考基因组进行比对。大多数用户会有自己物种的参考基因组。您可以在第五章与基因组工作中了解更多关于参考基因组的信息。

对齐数据最常见的表示方法是 SAMtools 的tabix工具。SAMtools 可能是最广泛使用的 SAM/BAM 文件操作工具。

准备工作

如前一篇食谱中所讨论的,我们将使用来自 1,000 基因组计划的数据。我们将使用女性NA18489的染色体 20 外显子对齐数据。数据大小为 312 MB。该个体的全外显子对齐数据为 14.2 吉字节GB),全基因组对齐数据(低覆盖度为 4x)为 40.1 GB。该数据为双端读取,读取长度为 76 bp。如今这很常见,但处理起来稍微复杂一些。我们会考虑这一点。如果您的数据不是双端数据,可以适当简化以下食谱。

Chapter03/Working_with_BAM.py文件顶部的单元格将为您下载数据。您需要的文件是NA18490_20_exome.bamNA18490_20_exome.bam.bai

我们将使用pysam,它是 SAMtools C API 的 Python 包装器。您可以通过以下命令安装它:

conda install –c bioconda pysam

好的—让我们开始吧。

如何操作...

在开始编码之前,请注意,您可以使用samtools view -h检查 BAM 文件(前提是您已经安装了 SAMtools,我们推荐您安装,即使您使用的是基因组分析工具包GATK)或其他变异调用工具)。我们建议您查看头文件和前几个记录。SAM 格式过于复杂,无法在这里描述。网上有很多关于它的信息;不过,有时,某些非常有趣的信息就隐藏在这些头文件中。

提示

NGS 中最复杂的操作之一是从原始序列数据生成良好的比对文件。这不仅涉及调用比对工具,还包括清理数据。在高质量的 BAM 文件的 @PG 头部,你将找到用于生成该 BAM 文件的绝大多数(如果不是全部)过程的实际命令行。在我们的示例 BAM 文件中,你会找到所有运行 bwa、SAMtools、GATK IndelRealigner 和 Picard 应用程序套件来清理数据所需的信息。记住,虽然你可以轻松生成 BAM 文件,但之后的程序对于 BAM 输入的正确性会非常挑剔。例如,如果你使用 GATK 的变异调用器来生成基因型调用,文件必须经过广泛的清理。因此,其他 BAM 文件的头部可以为你提供生成自己文件的最佳方式。最后的建议是,如果你不处理人类数据,尝试为你的物种找到合适的 BAM 文件,因为某些程序的参数可能略有不同。此外,如果你使用的是非 WGS 数据,检查类似类型的测序数据。

让我们看看以下步骤:

  1. 让我们检查一下头文件:

    import pysam
    bam = pysam.AlignmentFile('NA18489.chrom20.ILLUMINA.bwa.YRI.exome.20121211.bam', 'rb')
    headers = bam.header
    for record_type, records in headers.items():
        print (record_type)
        for i, record in enumerate(records):
            if type(record) == dict:
                print('\t%d' % (i + 1))
                for field, value in record.items():
                    print('\t\t%s\t%s' % (field, value))
            else:
                print('\t\t%s' % record)
    

头部被表示为字典(其中键是 record_type)。由于同一 record_type 可能有多个实例,字典的值是一个列表(其中每个元素再次是一个字典,或者有时是包含标签/值对的字符串)。

  1. 现在,我们将检查一个单一的记录。每个记录的数据量相当复杂。在这里,我们将重点关注配对末端读取的一些基本字段。有关更多详细信息,请查看 SAM 文件规范和 pysam API 文档:

    for rec in bam:
        if rec.cigarstring.find('M') > -1 and rec.cigarstring.find('S') > -1 and not rec.is_unmapped and not rec.mate_is_unmapped:
        break
    print(rec.query_name, rec.reference_id, bam.getrname(rec.reference_id), rec.reference_start, rec.reference_end)
    print(rec.cigarstring)
    print(rec.query_alignment_start, rec.query_alignment_end, rec.query_alignment_length)
    print(rec.next_reference_id, rec.next_reference_start,rec.template_length)
    print(rec.is_paired, rec.is_proper_pair, rec.is_unmapped, rec.mapping_quality)
    print(rec.query_qualities)
    print(rec.query_alignment_qualities)
    print(rec.query_sequence)
    

请注意,BAM 文件对象可以通过其记录进行迭代。我们将遍历它,直到找到一个 Concise Idiosyncratic Gapped Alignment ReportCIGAR)字符串包含匹配和软剪切的记录。

CIGAR 字符串给出了单个碱基的比对信息。序列中被剪切的部分是比对工具未能对齐的部分(但未从序列中删除)。我们还需要读取序列、其配对 ID 以及映射到参考基因组上的位置(由于我们有配对末端读取,因此是配对的位置信息)。

首先,我们打印查询模板名称,接着是参考 ID。参考 ID 是指向给定参考序列查找表中序列名称的指针。一个示例可以使这点更加清晰。对于该 BAM 文件中的所有记录,参考 ID 是19(一个没有实际意义的数字),但如果你应用bam.getrname(19),你会得到20,这就是染色体的名称。所以,不要将参考 ID(此处是19)与染色体名称(20)混淆。接下来是参考开始和参考结束。pysam是基于 0 的,而非基于 1 的,所以在将坐标转换为其他库时要小心。你会注意到,在这个例子中,开始和结束的位置分别是 59,996 和 60,048,这意味着一个 52 碱基的比对。为什么当读取大小是 76(记住,这是该 BAM 文件中使用的读取大小)时,只有 52 个碱基?答案可以通过 CIGAR 字符串找到,在我们的例子中是52M24S,即 52 个匹配碱基,后面是 24 个软剪接的碱基。

然后,我们打印比对的开始和结束位置,并计算其长度。顺便说一句,你可以通过查看 CIGAR 字符串来计算这一点。它从 0 开始(因为读取的第一部分已被比对),并在 52 处结束。长度再次是 76。

现在,我们查询配对端(如果你有双端读取,才会做这个操作)。我们获取它的参考 ID(如前面的代码片段所示),它的开始位置,以及两个配对之间的距离度量。这个距离度量只有在两个配对都映射到同一染色体时才有意义。

接着,我们绘制序列的 Phred 分数(参见之前的配方,处理现代序列格式,关于 Phred 分数的部分),然后仅绘制已比对部分的 Phred 分数。最后,我们打印出该序列(别忘了这么做!)。这是完整的序列,而不是剪接过的序列(当然,你可以使用之前的坐标来进行剪接)。

  1. 现在,让我们在 BAM 文件中的一个子集序列中绘制成功映射位置的分布:

    import seaborn as sns
    import matplotlib.pyplot as plt
    counts = [0] * 76
    for n, rec in enumerate(bam.fetch('20', 0, 10000000)):
        for i in range(rec.query_alignment_start, rec.query_alignment_end):
            counts[i] += 1
    freqs = [x / (n + 1.) for x in counts]
    fig, ax = plt.subplots(figsize=(16,9))
    ax.plot(range(1, 77), freqs)
    ax.set_xlabel('Read distance')
    ax.set_ylabel('PHRED score')
    fig.suptitle('Percentage of mapped calls as a function of the position from the start of the sequencer read')
    

我们将首先初始化一个数组,用来保存整个76个位置的计数。请注意,我们接下来只获取染色体 20 上从位置 0 到 10 的记录(tabix)进行此类抓取操作;执行速度将会完全不同。

我们遍历所有位于 10 Mbp 边界内的记录。对于每个边界,我们获取比对的开始和结束,并增加在这些已比对位置中的映射性计数器。最后,我们将其转换为频率,然后进行绘制,如下图所示:

图 3.3 – 映射调用的百分比与测序器读取开始位置的函数关系

图 3.3 – 映射调用的百分比与测序器读取开始位置的函数关系

很明显,映射性分布远非均匀;在极端位置更差,中间位置则出现下降。

  1. 最后,让我们获取映射部分的 Phred 分数分布。正如你可能猜到的,这可能不会是均匀分布的:

    from collections import defaultdict
    import numpy as np
    phreds = defaultdict(list)
    for rec in bam.fetch('20', 0, None):
        for i in range(rec.query_alignment_start, rec.query_alignment_end):
            phreds[i].append(rec.query_qualities[i])
    maxs = [max(phreds[i]) for i in range(76)]
    tops = [np.percentile(phreds[i], 95) for i in range(76)]
    medians = [np.percentile(phreds[i], 50) for i in range(76)]
    bottoms = [np.percentile(phreds[i], 5) for i in range(76)]
    medians_fig = [x - y for x, y in zip(medians, bottoms)]
    tops_fig = [x - y for x, y in zip(tops, medians)]
    maxs_fig = [x - y for x, y in zip(maxs, tops)]
    fig, ax = plt.subplots(figsize=(16,9))
    ax.stackplot(range(1, 77), (bottoms, medians_fig,tops_fig))
    ax.plot(range(1, 77), maxs, 'k-')
    ax.set_xlabel('Read distance')
    ax.set_ylabel('PHRED score')
    fig.suptitle('Distribution of PHRED scores as a function of the position in the read')
    

在这里,我们再次使用默认字典,它允许你使用一些初始化代码。我们现在从开始到结束提取数据,并在字典中创建一个 Phred 分数的列表,其中索引是序列读取中的相对位置。

然后,我们使用 NumPy 计算每个位置的 95 百分位、50 百分位(中位数)和 5 百分位,以及质量分数的最大值。对于大多数计算生物学分析,拥有数据的统计汇总视图是非常常见的。因此,你可能不仅熟悉百分位数的计算,还熟悉其他 Pythonic 方式来计算均值、标准差、最大值和最小值。

最后,我们将绘制每个位置的 Phred 分数的堆叠图。由于matplotlib期望堆叠的方式,我们必须通过 stackplot 调用从前一个百分位值中减去较低百分位的值。我们可以使用底部百分位数的列表,但我们需要修正中位数和顶部百分位,如下所示:

图 3.4 – Phred 分数在读取位置上的分布;底部蓝色表示从 0 到 5 百分位;绿色表示中位数,红色表示 95 百分位,紫色表示最大值

图 3.4 – Phred 分数在读取位置上的分布;底部蓝色表示从 0 到 5 百分位;绿色表示中位数,红色表示 95 百分位,紫色表示最大值

还有更多...

虽然我们将在本章的学习基因组可达性与过滤 SNP 数据配方中讨论数据过滤,但我们的目标并不是详细解释 SAM 格式或给出数据过滤的详细课程。这项任务需要一本专门的书籍,但凭借pysam的基础,你可以浏览 SAM/BAM 文件。不过,在本章的最后一个配方中,我们将探讨如何从 BAM 文件中提取全基因组度量(通过表示 BAM 文件度量的 VCF 文件注释),目的是了解我们数据集的整体质量。

你可能会有非常大的数据文件需要处理。有可能某些 BAM 处理会花费太多时间。减少计算时间的第一个方法是抽样。例如,如果你以 10% 进行抽样,你将忽略 10 条记录中的 9 条。对于许多任务,比如一些 BAM 文件质量评估的分析,以 10%(甚至 1%)进行抽样就足够获得文件质量的大致情况。

如果你使用的是人类数据,你可能会在 Complete Genomics 上进行测序。在这种情况下,比对文件将会有所不同。尽管 Complete Genomics 提供了将数据转换为标准格式的工具,但如果使用其自己的数据,可能会更适合你。

另见

额外的信息可以通过以下链接获得:

从 VCF 文件中提取数据

在运行基因型调用工具(例如,GATK 或 SAMtools)后,你将得到一个 VCF 文件,报告基因组变异信息,如 SNPs,cyvcf2模块。

准备工作

尽管 NGS 主要涉及大数据,但我不能要求你为本书下载过多的数据集。我认为 2 到 20 GB 的数据对于教程来说太多了。虽然 1,000 基因组计划的 VCF 文件和实际注释数据在这个数量级,但我们这里将处理更少的数据。幸运的是,生物信息学社区已经开发了工具,允许部分下载数据。作为 SAMtools/htslib包的一部分(www.htslib.org/),你可以下载tabixbgzip,它们将负责数据管理。在命令行中,执行以下操作:

tabix -fh ftp://ftp-
trace.ncbi.nih.gov/1000genomes/ftp/release/20130502/supporting/vcf_with_sample_level_annotation/ALL.chr22.phase3_shapeit2_mvncall_integrated_v5_extra_anno.20130502.genotypes.vcf.gz 22:1-17000000 | bgzip -c > genotypes.vcf.gz
tabix -p vcf genotypes.vcf.gz

第一行将部分下载来自 1,000 基因组计划的 22 号染色体 VCF 文件(最多 17 Mbp),然后,bgzip会进行压缩。

第二行将创建一个索引,这是我们直接访问基因组某一部分所需要的。像往常一样,你可以在笔记本中找到执行此操作的代码(Chapter03/Working_with_VCF.py文件)。

你需要安装cyvcf2

conda install –c bioconda cyvcf2

提示

如果你遇到冲突解决问题,可以尝试改用 pip。这是一个最后的解决方案,当你使用 conda 时,往往会因为其无法解决软件包依赖问题而不得不这样做,你可以执行 pip install cyvcf2

如何操作……

请查看以下步骤:

  1. 让我们从检查每条记录可以获取的信息开始:

    from cyvcf2 import VCF
    v = VCF('genotypes.vcf.gz')
    rec = next(v)
    print('Variant Level information')
    info = rec.INFO
    for info in rec.INFO:
        print(info)
    print('Sample Level information')
    for fmt in rec.FORMAT:
        print(fmt)
    

我们从检查每条记录可用的注释开始(记住,每条记录编码一个变异,例如 SNP、CNV、INDEL 等,以及该变异在每个样本中的状态)。在变异(记录)级别,我们会找到 AC —— 在调用基因型中 ALT 等位基因的总数,AF —— 估算的等位基因频率,NS —— 有数据的样本数,AN —— 在调用基因型中的等位基因总数,以及 DP —— 总读取深度。还有其他信息,但它们大多数是 1000 基因组计划特有的(在这里,我们将尽量保持通用)。你自己的数据集可能有更多的注释(或没有这些注释)。

在样本级别,这个文件中只有两个注释:GT —— 基因型,和 DP —— 每个样本的读取深度。你有每个变异的总读取深度和每个样本的读取深度,请确保不要混淆两者。

  1. 现在我们知道了有哪些信息可用,让我们查看单个 VCF 记录:

    v = VCF('genotypes.vcf.gz')
    samples = v.samples
    print(len(samples))
    variant = next(v)
    print(variant.CHROM, variant.POS, variant.ID, variant.REF, variant.ALT, variant.QUAL, variant.FILTER)
    print(variant.INFO)
    print(variant.FORMAT)
    print(variant.is_snp)
    str_alleles = variant.gt_bases[0]
    alleles = variant.genotypes[0][0:2]
    is_phased = variant.genotypes[0][2]
    print(str_alleles, alleles, is_phased)
    print(variant.format('DP')[0])
    

我们将从获取标准信息开始:染色体、位置、ID、参考碱基(通常只有一个)和替代碱基(可以有多个,但作为一种常见的初步筛选方法,通常只接受单个 ALT,例如只接受双等位基因 SNP),质量(如你所预期,采用 Phred 扩展评分),以及过滤状态。关于过滤状态,请记住,不论 VCF 文件中如何表示,你可能仍然需要应用额外的过滤器(如下一个教程所述,研究基因组可访问性和筛选 SNP 数据)。

然后,我们打印附加的变异级别信息(ACASAFANDP 等),接着是样本格式(在此案例中为 DPGT)。最后,我们统计样本数并检查单个样本,以确认它是否针对该变异进行了调用。同时,还包括报告的等位基因、杂合性和相位状态(该数据集恰好是相位的,虽然这并不常见)。

  1. 让我们一次性检查变异的类型和非双等位基因 SNP 的数量:

    from collections import defaultdict
    f = VCF('genotypes.vcf.gz')
    my_type = defaultdict(int)
    num_alts = defaultdict(int)
    for variant in f:
        my_type[variant.var_type, variant.var_subtype] += 1
        if variant.var_type == 'snp':
            num_alts[len(variant.ALT)] += 1
    print(my_type)
    

我们将使用现在常见的 Python 默认字典。我们发现该数据集中包含 INDEL、CNV 和——当然——SNP(大约三分之二是转换突变,一三分之一是倒位突变)。还有一个剩余的数量(79)为三等位基因 SNP。

还有更多内容……

本教程的目的是让你快速熟悉 cyvcf2 模块。此时,你应该已经能熟练使用该 API。我们不会花太多时间讲解使用细节,因为下一个教程的主要内容是:使用 VCF 模块研究变异调用的质量。

虽然cyvcf2非常快速,但处理基于文本的 VCF 文件仍然可能需要很长时间。有两种主要策略来处理这个问题。一种策略是并行处理,我们将在最后一章第九章生物信息学管道中讨论。第二种策略是转换为更高效的格式;我们将在第六章群体遗传学中提供示例。请注意,VCF 开发人员正在开发二进制变异调用格式BCF)版本,以解决这些问题的部分内容(www.1000genomes.org/wiki/analysis/variant-call-format/bcf-binary-vcf-version-2)。

另见

一些有用的链接如下:

研究基因组可及性和过滤 SNP 数据

虽然之前的配方集中于提供 Python 库的概述,用于处理比对和变异调用数据,但在这篇配方中,我们将专注于实际使用这些库,并明确目标。

如果你正在使用 NGS 数据,很可能你要分析的最重要文件是 VCF 文件,它由基因型调用器(如 SAMtools、mpileup或 GATK)生成。你可能需要评估和过滤你的 VCF 调用的质量。在这里,我们将建立一个框架来过滤 SNP 数据。我们不会提供具体的过滤规则(因为在一般情况下无法执行这一任务),而是给出评估数据质量的程序。通过这些程序,你可以设计自己的过滤规则。

准备工作

在最佳情况下,你会有一个应用了适当过滤的 VCF 文件。如果是这种情况,你可以直接使用你的文件。请注意,所有 VCF 文件都会有一个FILTER列,但这可能并不意味着所有正确的过滤都已应用。你必须确保数据已正确过滤。

在第二种情况中,这是最常见的一种情况,你的文件将包含未过滤的数据,但你会有足够的注释,并且可以应用硬过滤(无需程序化过滤)。如果你有 GATK 注释的文件,可以参考gatkforums.broadinstitute.org/discussion/2806/howto-apply-hard-filters-to-a-call-set

在第三种情况下,你有一个包含所有必要注释的 VCF 文件,但你可能希望应用更灵活的过滤器(例如,“如果读取深度 > 20,则映射质量 > 30 时接受;否则,映射质量 > 40 时接受”)。

在第四种情况下,你的 VCF 文件缺少所有必要的注释,你必须重新检查 BAM 文件(甚至其他信息来源)。在这种情况下,最好的解决方案是找到任何额外的信息,并创建一个带有所需注释的新 VCF 文件。一些基因型调用器(如 GATK)允许你指定需要哪些注释;你可能还需要使用额外的程序来提供更多的注释。例如,SnpEff (snpeff.sourceforge.net/) 会为你的 SNPs 注释其效应预测(例如,如果它们位于外显子中,它们是编码区还是非编码区?)。

提供一个明确的配方是不可能的,因为它会根据你的测序数据类型、研究物种以及你对错误的容忍度等变量有所不同。我们能做的是提供一套典型的高质量过滤分析方法。

在这个配方中,我们不会使用来自人类 1000 基因组计划的数据。我们想要的是的、未过滤的数据,其中有许多常见的注释可以用来进行过滤。我们将使用来自按蚊 1000 基因组计划的数据(按蚊是传播疟疾寄生虫的蚊子载体),该计划提供了过滤和未过滤的数据。你可以在www.malariagen.net/projects/vector/ag1000g上找到有关此项目的更多信息。

我们将获取大约 100 只蚊子的染色体3L的部分着丝粒区域,接着获取该染色体中间某部分(并索引两者):

tabix -fh ftp://ngs.sanger.ac.uk/production/ag1000g/phase1/preview/ag1000g.AC.phase1.AR1.vcf.gz 3L:1-200000 |bgzip -c > centro.vcf.gz
tabix -fh ftp://ngs.sanger.ac.uk/production/ag1000g/phase1/preview/ag1000g.AC.phase1.AR1.vcf.gz 3L:21000001-21200000 |bgzip -c > standard.vcf.gz
tabix -p vcf centro.vcf.gz
tabix -p vcf standard.vcf.gz

如果链接无法使用,请确保查看github.com/PacktPublishing/Bioinformatics-with-Python-Cookbook-third-edition/blob/main/Datasets.py以获取更新。像往常一样,用于下载该数据的代码在Chapter02/Filtering_SNPs.ipynb笔记本中。

最后,关于这个配方的一个警告:这里的 Python 难度会比平时稍微复杂一些。我们编写的越通用的代码,你就越容易将其重用于你的特定情况。我们将广泛使用函数式编程技术(lambda函数)和partial函数应用。

如何做...

看一下以下步骤:

  1. 让我们从绘制两个文件中基因组变异分布的图表开始:

    from collections import defaultdict
    import functools
    import numpy as np
    import seaborn as sns
    import matplotlib.pyplot as plt
    from cyvcf2 import VCF
    def do_window(recs, size, fun):
        start = None
        win_res = []
        for rec in recs:
            if not rec.is_snp or len(rec.ALT) > 1:
                continue
            if start is None:
                start = rec.POS
            my_win = 1 + (rec.POS - start) // size
            while len(win_res) < my_win:
                win_res.append([])
            win_res[my_win - 1].extend(fun(rec))
        return win_res
    wins = {}
    size = 2000
    names = ['centro.vcf.gz', 'standard.vcf.gz']
    for name in names:
     recs = VCF(name)
     wins[name] = do_window(recs, size, lambda x: [1])
    

我们将从执行所需的导入开始(和往常一样,如果你不使用 IPython Notebook,请记得删除第一行)。在我解释功能之前,请注意我们正在做什么。

对于这两个文件,我们将计算窗口统计数据。我们将我们的数据文件(包含 200,000 bp 的数据)分成大小为 2,000 的窗口(100 个窗口)。每次找到一个双等位基因 SNP,我们将在window函数相关的列表中添加一个 1。

window函数将获取一个 VCF 记录(rec.is_snp表示不是双等位基因的 SNP,长度为(rec.ALT) == 1),确定该记录所属的窗口(通过将rec.POS整除大小来执行),并通过作为fun参数传递给它的函数(在我们的情况下,只是 1)扩展该窗口结果列表。

因此,现在我们有了一个包含 100 个元素的列表(每个代表 2,000 bp)。每个元素将是另一个列表,其中每个双等位基因 SNP 找到时将有一个 1。

因此,如果在前 2,000 bp 中有 200 个 SNP,列表的第一个元素将有 200 个 1。

  1. 让我们继续,如下所示:

    def apply_win_funs(wins, funs):
        fun_results = []
        for win in wins:
            my_funs = {}
            for name, fun in funs.items():
                try:
                    my_funs[name] = fun(win)
                except:
                    my_funs[name] = None
            fun_results.append(my_funs)
        return fun_results
    stats = {}
    fig, ax = plt.subplots(figsize=(16, 9))
    for name, nwins in wins.items():
        stats[name] = apply_win_funs(nwins, {'sum': sum})
        x_lim = [i * size for i in range(len(stats[name]))]
        ax.plot(x_lim, [x['sum'] for x in stats[name]], label=name)
    ax.legend()
    ax.set_xlabel('Genomic location in the downloaded segment')
    ax.set_ylabel('Number of variant sites (bi-allelic SNPs)')
    fig.suptitle('Number of bi-allelic SNPs along the genome', fontsize='xx-large')
    

在这里,我们执行一个包含每个 100 个窗口的统计信息的图表。apply_win_funs将为每个窗口计算一组统计数据。在这种情况下,它将对窗口中的所有数字求和。请记住,每次找到一个 SNP,我们都会在窗口列表中添加一个 1。这意味着如果我们有 200 个 SNP,我们将有 200 个 1;因此,将它们求和将返回 200。

因此,我们能够以一种显然复杂的方式计算每个窗口中的 SNP 数量。为什么我们用这种策略执行事情很快就会显而易见。但是,现在,让我们检查这两个文件的计算结果,如下屏幕截图所示:

图 3.5 – 位于染色体 3L 附近 200 千碱基对(kbp)区域的 2000 bp 大小的双等位基因 SNP 分布窗口(橙色),以及染色体中部(蓝色)的情况;这两个区域来自约 100 只乌干达按蚊的 Anopheles 1,000 基因组计划

图 3.5 – 位于染色体 3L 附近 200 千碱基对(kbp)区域的 2000 bp 大小的双等位基因 SNP 分布窗口(橙色),以及染色体中部(蓝色)的情况;这两个区域来自约 100 只乌干达按蚊的 Anopheles 1,000 基因组计划。

小贴士

注意,在中心粒的 SNP 数量比染色体中部少。这是因为在染色体中调用变异体比在中部更困难。此外,中心粒的基因组多样性可能较少。如果您习惯于人类或其他哺乳动物,您会发现变异体的密度非常高——这就是蚊子的特点!

  1. 让我们来看看样本级别的注释。我们将检查映射质量零(请参阅 www.broadinstitute.org/gatk/guide/tooldocs/org_broadinstitute_gatk_tools_walkers_annotator_MappingQualityZeroBySample.php 了解详情),这是衡量参与调用该变异的序列是否能够清晰地映射到此位置的一个指标。请注意,变异级别也有一个 MQ0 注释:

    mq0_wins = {}
    size = 5000
    def get_sample(rec, annot, my_type):
        return [v for v in rec.format(annot) if v > np.iinfo(my_type).min]
    for vcf_name in vcf_names:
        recs = vcf.Reader(filename=vcf_name)
        mq0_wins[vcf_name] = do_window(recs, size, functools.partial(get_sample, annot='MQ0', my_type=np.int32))
    

从检查最后一个 for 开始;我们将通过读取每个记录中的 MQ0 注释进行窗口分析。我们通过调用 get_sample 函数来执行此操作,该函数将返回我们首选的注释(在本例中为 MQ0),该注释已被转换为特定类型(my_type=np.int32)。我们在这里使用了 partial 应用函数。Python 允许您指定函数的某些参数,等待稍后再指定其他参数。请注意,这里最复杂的部分是函数式编程风格。此外,请注意,这使得计算其他样本级别的注释变得非常容易。只需将 MQ0 替换为 ABADGQ 等,您就能立即得到该注释的计算结果。如果该注释不是整数类型,也没问题;只需调整 my_type 即可。如果您不习惯这种编程风格,它可能比较困难,但您很快就会发现它的好处。

  1. 现在,让我们打印每个窗口的中位数和第 75 百分位数(在本例中,窗口大小为 5,000):

    stats = {}
    colors = ['b', 'g']
    i = 0
    fig, ax = plt.subplots(figsize=(16, 9))
    for name, nwins in mq0_wins.items():
        stats[name] = apply_win_funs(nwins, {'median':np.median, '75': functools.partial(np.percentile, q=75)})
        x_lim = [j * size for j in range(len(stats[name]))]
        ax.plot(x_lim, [x['median'] for x in stats[name]], label=name, color=colors[i])
        ax.plot(x_lim, [x['75'] for x in stats[name]], '--', color=colors[i])
        i += 1
    ax.legend()
    ax.set_xlabel('Genomic location in the downloaded segment')
    ax.set_ylabel('MQ0')
    fig.suptitle('Distribution of MQ0 along the genome', fontsize='xx-large')
    

请注意,现在我们在 apply_win_funs 上有两种不同的统计数据(百分位数和中位数)。我们再次将函数作为参数传递(np.mediannp.percentile),并在 np.percentile 上进行了 partial 函数应用。结果如下所示:

图 3.6 – 样本 SNP 的 MQ0 中位数(实线)和第 75 百分位数(虚线),这些 SNP 分布在每个窗口大小为 5,000 bp 的区域内,覆盖了位于着丝粒附近(蓝色)和染色体中部(绿色)的 200 kbp 区域;这两个区域来自 100 只左右乌干达蚊子的 3L 染色体,数据来自蚊子基因组 1,000 项目

图 3.6 – 样本 SNP 的 MQ0 中位数(实线)和第 75 百分位数(虚线),这些 SNP 分布在每个窗口大小为 5,000 bp 的区域内,覆盖了位于着丝粒附近(蓝色)和染色体中部(绿色)的 200 kbp 区域;这两个区域来自 100 只左右乌干达蚊子的 3L 染色体,数据来自蚊子基因组 1,000 项目。

对于 standard.vcf.gz 文件,中位数 MQ00(它绘制在图表底部,几乎不可见)。这是好的,表明大部分涉及变异调用的序列都能清晰地映射到基因组的这个区域。对于 centro.vcf.gz 文件,MQ0 的质量较差。此外,还有一些区域,基因型调用器无法找到任何变异(因此图表不完整)。

  1. 让我们将杂合性与DP(样本级注释)进行比较。在这里,我们将绘制每个 SNP 的杂合性调用比例与样本读取深度DP)的关系图。首先,我们将解释结果,然后是生成它的代码。

下一张截图显示了在某一深度下杂合体调用的比例:

图 3.7 – 连续线表示在某一深度计算的杂合体调用比例;橙色区域为着丝粒区域;蓝色区域为“标准”区域;虚线表示每个深度的样本调用数;这两个区域来自于安哥拉疟蚊 1000 基因组计划中大约 100 只乌干达蚊子的 3L 号染色体

图 3.7 – 连续线表示在某一深度计算的杂合体调用比例;橙色区域为着丝粒区域;蓝色区域为“标准”区域;虚线表示每个深度的样本调用数;这两个区域来自于安哥拉疟蚊 1000 基因组计划中大约 100 只乌干达蚊子的 3L 号染色体。

在前面的截图中,有两个因素需要考虑。在非常低的深度下,杂合体调用的比例是偏倚的——在这种情况下,它较低。这是有道理的,因为每个位置的读取次数不足以准确估计样本中两种等位基因的存在。因此,您不应该相信在非常低深度下的调用。

正如预期的那样,着丝粒区域的调用次数明显低于其外部区域。着丝粒外的 SNP 分布遵循许多数据集中常见的模式。

下面是这部分代码的展示:

def get_sample_relation(recs, f1, f2):
    rel = defaultdict(int)
    for rec in recs:
        if not rec.is_snp:
             continue
        for pos in range(len(rec.genotypes)):
            v1 = f1(rec, pos)
            v2 = f2(rec, pos)
            if v1 is None or v2 == np.iinfo(type(v2)).min:
                continue  # We ignore Nones
            rel[(v1, v2)] += 1
            # careful with the size, floats: round?
        #break
    return rel get_sample_relation(recs, f1, f2):
rels = {}
for vcf_name in vcf_names:
    recs = VCF(filename=vcf_name)
    rels[vcf_name] = get_sample_relation(
        recs,
        lambda rec, pos: 1 if rec.genotypes[pos][0] != rec.genotypes[pos][1] else 0,
        lambda rec, pos: rec.format('DP')[pos][0])

首先,寻找for循环。我们再次使用函数式编程;get_sample_relation函数将遍历所有 SNP 记录并应用两个函数参数。第一个参数确定杂合性,第二个参数获取样本的DP(记住,DP也有变种)。

现在,由于代码本身相当复杂,我选择使用一个朴素的数据结构来返回get_sample_relation:一个字典,键是结果对(在此案例中为杂合性和DP),值是共享这两个值的 SNP 总数。还有更优雅的数据结构,具有不同的权衡。比如,您可以使用 SciPy 稀疏矩阵、pandas DataFrame,或者考虑使用 PyTables。这里的关键是要有一个足够通用的框架来计算样本注释之间的关系。

此外,注意多个注释的维度空间。例如,如果您的注释是浮动类型,您可能需要对其进行四舍五入(如果不进行处理,数据结构的大小可能会变得过大)。

  1. 现在,让我们来看一下绘图代码。我们分两部分来进行。以下是第一部分:

    def plot_hz_rel(dps, ax, ax2, name, rel):
        frac_hz = []
        cnt_dp = []
        for dp in dps:
            hz = 0.0
            cnt = 0
            for khz, kdp in rel.keys():
                if kdp != dp:
                    continue
                cnt += rel[(khz, dp)]
                if khz == 1:
                    hz += rel[(khz, dp)]
            frac_hz.append(hz / cnt)
            cnt_dp.append(cnt)
        ax.plot(dps, frac_hz, label=name)
        ax2.plot(dps, cnt_dp, '--', label=name)
    

该函数将接受由get_sample_relation生成的数据结构,假定关键元组的第一个参数是杂合状态(0=纯合子,1=杂合子),第二个参数是DP。这样,它将生成两条数据:一条表示在某一深度下为杂合子的样本比例,另一条表示 SNP 数量。

  1. 现在,让我们调用这个函数:

    fig, ax = plt.subplots(figsize=(16, 9))
    ax2 = ax.twinx()
    for name, rel in rels.items():
        dps = list(set([x[1] for x in rel.keys()]))
    dps.sort()
    plot_hz_rel(dps, ax, ax2, name, rel)
    ax.set_xlim(0, 75)
    ax.set_ylim(0, 0.2)
    ax2.set_ylabel('Quantity of calls')
    ax.set_ylabel('Fraction of Heterozygote calls')
    ax.set_xlabel('Sample Read Depth (DP)')
    ax.legend()
    fig.suptitle('Number of calls per depth and fraction of calls which are Hz', fontsize='xx-large')
    

在这里,我们将使用两个坐标轴。左边是杂合 SNP 的比例,右边是 SNP 的数量。然后,我们为两个数据文件调用plot_hz_rel。其余部分是标准的matplotlib代码。

  1. 最后,让我们将DP变异与类别变异级注释(EFF)进行比较。EFF由 SnpEff 提供,告诉我们(以及其他许多信息)SNP 类型(例如,基因间、内含子、同义编码和非同义编码)。疟蚊数据集提供了这个有用的注释。让我们从提取变异级注释和功能编程样式开始:

    def get_variant_relation(recs, f1, f2):
        rel = defaultdict(int)
        for rec in recs:
            if not rec.is_snp:
                continue
        try:
            v1 = f1(rec)
            v2 = f2(rec)
            if v1 is None or v2 is None:
                continue # We ignore Nones
            rel[(v1, v2)] += 1
        except:
            pass
        return rel
    

这里的编程风格类似于get_sample_relation,但我们不会深入讨论任何样本。现在,我们定义我们将处理的效应类型,并将其效应转换为整数(因为这将允许我们将其作为索引使用——例如,矩阵)。现在,考虑为类别变量编写代码:

accepted_eff = ['INTERGENIC', 'INTRON', 'NON_SYNONYMOUS_CODING', 'SYNONYMOUS_CODING']
def eff_to_int(rec):
    try:
        annot = rec.INFO['EFF']
        master_type = annot.split('(')[0]
        return accepted_eff.index(master_type)
    except ValueError:
        return len(accepted_eff)
  1. 我们现在将遍历文件;样式现在应该对你清晰可见:

    eff_mq0s = {}
    for vcf_name in vcf_names:
        recs = VCF(filename=vcf_name)
        eff_mq0s[vcf_name] = get_variant_relation(recs, lambda r: eff_to_int(r), lambda r: int(r.INFO['DP']))
    
  2. 最后,我们使用 SNP 效应绘制DP的分布:

    fig, ax = plt.subplots(figsize=(16,9))
    vcf_name = 'standard.vcf.gz'
    bp_vals = [[] for x in range(len(accepted_eff) + 1)]
    for k, cnt in eff_mq0s[vcf_name].items():
        my_eff, mq0 = k
        bp_vals[my_eff].extend([mq0] * cnt)
    sns.boxplot(data=bp_vals, sym='', ax=ax)
    ax.set_xticklabels(accepted_eff + ['OTHER'])
    ax.set_ylabel('DP (variant)')
    fig.suptitle('Distribution of variant DP per SNP type', fontsize='xx-large')
    

在这里,我们仅为非着丝粒文件打印一个箱线图,如下面的图所示。结果符合预期:编码区域中的 SNP 可能会有更高的深度,因为它们位于更复杂的区域,这些区域比基因间的 SNP 更容易被调用:

图 3.8 – 不同 SNP 效应下变异读取深度的箱线图

图 3.8 – 不同 SNP 效应下变异读取深度的箱线图

还有更多内容...

关于过滤 SNP 和其他基因组特征的问题,需要一本书来详细讲解。这个方法将取决于你所拥有的测序数据类型、样本数量以及潜在的附加信息(例如,样本之间的家系关系)。

这个方法本身非常复杂,但其中一些部分非常简单(在一个简单的配方中,我无法强行加上过于复杂的内容)。例如,窗口代码不支持重叠窗口。并且,数据结构相对简单。然而,我希望它们能给你提供一种处理基因组高通量测序数据的整体策略。你可以在第四章高级 NGS 处理中阅读更多内容。

另见

更多信息可以通过以下链接找到:

  • 有许多过滤规则,但我想特别提醒你注意需要有足够好的覆盖度(显然需要超过 10x)。请参考Meynert et al.的论文《全基因组和外显子组测序中的变异检测灵敏度与偏差》,网址:www.biomedcentral.com/1471-2105/15/247/

  • bcbio-nextgen是一个基于 Python 的高通量测序分析管道,值得一试 (bcbio-nextgen.readthedocs.org)。

使用 HTSeq 处理 NGS 数据

HTSeq(htseq.readthedocs.io)是一个用于处理 NGS 数据的替代库。HTSeq 提供的大部分功能实际上在本书中覆盖的其他库中也有,但是你应该知道它作为一种替代方式来处理 NGS 数据。HTSeq 支持包括 FASTA、FASTQ、SAM(通过pysam)、VCF、通用特征格式GFF)和浏览器可扩展数据BED)文件格式等。它还包括一组用于处理(映射)基因组数据的抽象概念,涵盖了如基因组位置、区间或比对等概念。由于本书无法详细探讨该库的所有功能,我们将专注于其中的一小部分功能,并借此机会介绍 BED 文件格式。

BED 格式允许为注释轨道指定特征。它有许多用途,但常见的用途是将 BED 文件加载到基因组浏览器中以可视化特征。每行包含关于至少位置(染色体、起始和结束)的信息,也包括可选字段,如名称或链。关于该格式的详细信息可以在genome.ucsc.edu/FAQ/FAQformat.xhtml#format1找到。

准备工作

我们的简单示例将使用来自人类基因组中 LCT 基因所在区域的数据。LCT 基因编码乳糖酶,这是一种参与乳糖消化的酶。

我们将从 Ensembl 获取这些信息。请访问uswest.ensembl.org/Homo_sapiens/Gene/Summary?db=core;g=ENSG00000115850,并选择LCT.bed文件,该文件位于Chapter03目录中。

这段代码的笔记本文件名为Chapter03/Processing_BED_with_HTSeq.py

在开始之前,先查看一下文件。这里提供了该文件几行内容的示例:

track name=gene description="Gene information"
 2       135836529       135837180       ENSE00002202258 0       -
 2       135833110       135833190       ENSE00001660765 0       -
 2       135789570       135789798       NM_002299.2.16  0       -
 2       135787844       135788544       NM_002299.2.17  0       -
 2       135836529       135837169       CCDS2178.117    0       -
 2       135833110       135833190       CCDS2178.116    0       -

第四列是特征名称。这个名称在不同的文件中会有很大的不同,你需要每次都检查它。然而,在我们的案例中,似乎显而易见的是我们有 Ensembl 外显子(ENSE...)、GenBank 记录(NM_...)以及来自共识编码序列CCDS)数据库的编码区信息(www.ncbi.nlm.nih.gov/CCDS/CcdsBrowse.cgi)。

你需要安装 HTSeq:

conda install –c bioconda htseq

现在,我们可以开始了。

如何做到...

看一下以下步骤:

  1. 我们将首先为文件设置一个读取器。记住,这个文件已经提供给你,并且应该在你的当前工作目录中:

    from collections import defaultdict
    import re
    import HTSeq
    lct_bed = HTSeq.BED_Reader('LCT.bed')
    
  2. 现在我们将通过它们的名称提取所有类型的特征:

    feature_types = defaultdict(int)
    for rec in lct_bed:
        last_rec = rec
        feature_types[re.search('([A-Z]+)', rec.name).group(0)] += 1
    print(feature_types)
    

记住,这段代码是特定于我们的例子。你需要根据你的情况调整它。

提示

你会发现前面的代码使用了 正则表达式 (regex) 。使用正则表达式时要小心,因为它们往往生成只读代码,难以维护。你可能会找到更好的替代方案。不管怎样,正则表达式是存在的,你会时不时遇到它们。

在我们的案例中,输出结果如下所示:

defaultdict(<class 'int'>, {'ENSE': 27, 'NM': 17, 'CCDS': 17})
  1. 我们保存了最后一条记录,以便检查它:

    print(last_rec)
    print(last_rec.name)
    print(type(last_rec))
    interval = last_rec.iv
    print(interval)
    print(type(interval))
    

有许多可用的字段,最显著的是 nameinterval。对于前面的代码,输出如下所示:

<GenomicFeature: BED line 'CCDS2178.11' at 2: 135788543 -> 135788322 (strand '-')>
 CCDS2178.11
 <class 'HTSeq.GenomicFeature'>
 2:[135788323,135788544)/-
 <class 'HTSeq._HTSeq.GenomicInterval'>
  1. 让我们深入研究这个区间:

    print(interval.chrom, interval.start, interval.end)
    print(interval.strand)
    print(interval.length)
    print(interval.start_d)
    print(interval.start_as_pos)
    print(type(interval.start_as_pos))
    

输出如下所示:

2 135788323 135788544
 -
 221
 135788543
 2:135788323/-
 <class 'HTSeq._HTSeq.GenomicPosition'>

注意基因组位置(染色体、起始位置和结束位置)。最复杂的问题是如何处理链。如果特征是编码在负链上,你需要小心处理。HTSeq 提供了 start_dend_d 字段来帮助你处理这个问题(也就是说,如果链是负链,起始和结束位置会被反转)。

最后,让我们从编码区域(CCDS 记录)中提取一些统计信息。我们将使用 CCDS,因为它可能比这里的策划数据库更好:

exon_start = None
exon_end = None
sizes = []
for rec in lct_bed:
    if not rec.name.startswith('CCDS'):
        continue
    interval = rec.iv
    exon_start = min(interval.start, exon_start or interval.start)
    exon_end = max(interval.length, exon_end or interval.end)
    sizes.append(interval.length)
sizes.sort()
print("Num exons: %d / Begin: %d / End %d" % (len(sizes), exon_start, exon_end))
print("Smaller exon: %d / Larger exon: %d / Mean size: %.1f" % (sizes[0], sizes[-1], sum(sizes)/len(sizes)))

输出应该是自我解释的:

Num exons: 17 / Begin: 135788323 / End 135837169
 Smaller exon: 79 / Larger exon: 1551 / Mean size: 340.2

还有更多...

BED 格式可能比这更复杂。此外,前面的代码是基于我们的文件内容的特定前提。不过,这个例子应该足够让你入门。即使在最糟糕的情况下,BED 格式也不是特别复杂。

HTSeq 的功能远不止这些,但这个例子主要作为整个包的起点。HTSeq 具有可以替代我们到目前为止介绍的大部分配方的功能。

第五章:高级 NGS 数据处理

如果您使用下一代测序NGS)数据,您会知道质量分析和处理是获取结果中的两个主要时间消耗。在本章的第一部分,我们将通过使用包含亲属信息的数据集来深入探讨 NGS 分析 - 在我们的情况下,是一个母亲、一个父亲和大约 20 个后代的数据集。这是进行质量分析的常见技术,因为家系信息将允许我们推断出我们的过滤规则可能产生的错误数量。我们还将利用同一数据集来查找基于现有注释的基因组特征。

本章的最后一个配方将深入探讨使用 NGS 数据的另一个高级主题:宏基因组学。我们将使用 QIIME2,一个用于宏基因组学的 Python 包,来分析数据。

如果您使用 Docker,请使用 tiagoantao/bioinformatics_base 镜像。有关 QIIME2 内容的特殊设置过程将在相关配方中讨论。

本章包括以下配方:

  • 准备用于分析的数据集

  • 使用门利因错误信息进行质量控制

  • 使用标准统计方法探索数据

  • 从测序注释中找到基因组特征

  • 使用 QIIME2 进行宏基因组学

准备用于分析的数据集

我们的起点将是一个 VCF 文件(或等效文件),其中包含由基因分析工具(在我们的情况下是基因组分析工具包GATK))进行的调用,包括注释信息。因为我们将过滤 NGS 数据,我们需要可靠的决策标准来调用一个位点。那么,我们如何获取这些信息?一般来说,我们做不到,但如果我们确实需要这样做,有三种基本方法:

  • 使用更强大的测序技术进行比较 - 例如,使用 Sanger 测序验证 NGS 数据集。这种方法成本高昂,只能用于少数基因位点。

  • 对于测序密切相关的个体,例如两个父母及其后代。在这种情况下,我们使用门利因遗传规则来决定某个调用是否可接受。这是人类基因组计划和安非蚊 1000 基因组计划均采用的策略。

  • 最后,我们可以使用模拟。这种设置不仅相当复杂,而且可靠性存疑。这更多是一个理论选项。

在本章中,我们将使用第二个选项,基于安非蚊 1000 基因组计划。该项目提供了基于蚊子杂交的信息。一个杂交会包括父母(母亲和父亲)以及最多 20 个后代。

在这个配方中,我们将准备我们的数据,以便在后续配方中使用。

准备就绪

我们将以 HDF5 格式下载文件以加快处理速度。请注意,这些文件相当大;您需要良好的网络连接和充足的磁盘空间:

wget -c ftp://ngs.sanger.ac.uk/production/ag1000g/phase1/AR3/variation/main/hdf5/ag1000g.phase1.ar3.pass.3L.h5
wget -c ftp://ngs.sanger.ac.uk/production/ag1000g/phase1/AR3/variation/main/hdf5/ag1000g.phase1.ar3.pass.2L.h5

这些文件有四个交叉,每个交叉大约有 20 个后代。我们将使用染色体臂 3L 和 2L。在这一阶段,我们也计算孟德尔误差(这是下一个食谱的主题,因此我们将在那时详细讨论)。

相关的笔记本文件是Chapter04/Preparation.py。目录中还有一个名为samples.tsv的本地样本元数据文件。

如何操作……

下载数据后,按照以下步骤操作:

  1. 首先,从一些导入开始:

    import pickle
    import gzip
    import random
    import numpy as np
    import h5py
    import pandas as pd
    
  2. 让我们获取样本元数据:

    samples = pd.read_csv('samples.tsv', sep='\t')
    print(len(samples))
    print(samples['cross'].unique())
    print(samples[samples['cross'] == 'cross-29-2'][['id', 'function']])
    print(len(samples[samples['cross'] == 'cross-29-2']))
    print(samples[samples['function'] == 'parent'])
    

我们还打印一些关于我们将要使用的交叉和所有父母的基本信息。

  1. 我们准备根据其 HDF5 文件处理染色体臂 3L:

    h5_3L = h5py.File('ag1000g.crosses.phase1.ar3sites.3L.h5', 'r')
    samples_hdf5 = list(map(lambda sample: sample.decode('utf-8'), h5_3L['/3L/samples']))
    calldata_genotype = h5_3L['/3L/calldata/genotype']
    MQ0 = h5_3L['/3L/variants/MQ0']
    MQ = h5_3L['/3L/variants/MQ']
    QD = h5_3L['/3L/variants/QD']
    Coverage = h5_3L['/3L/variants/Coverage']
    CoverageMQ0 = h5_3L['/3L/variants/CoverageMQ0']
    HaplotypeScore = h5_3L['/3L/variants/HaplotypeScore']
    QUAL = h5_3L['/3L/variants/QUAL']
    FS = h5_3L['/3L/variants/FS']
    DP = h5_3L['/3L/variants/DP']
    HRun = h5_3L['/3L/variants/HRun']
    ReadPosRankSum = h5_3L['/3L/variants/ReadPosRankSum']
    my_features = {
        'MQ': MQ,
        'QD': QD,
        'Coverage': Coverage,
        'HaplotypeScore': HaplotypeScore,
        'QUAL': QUAL,
        'FS': FS,
        'DP': DP,
        'HRun': HRun,
        'ReadPosRankSum': ReadPosRankSum
    }
    num_features = len(my_features)
    num_alleles = h5_3L['/3L/variants/num_alleles']
    is_snp = h5_3L['/3L/variants/is_snp']
    POS = h5_3L['/3L/variants/POS']
    
  2. 计算孟德尔误差的代码如下:

    #compute mendelian errors (biallelic)
    def compute_mendelian_errors(mother, father, offspring):
        num_errors = 0
        num_ofs_problems = 0
        if len(mother.union(father)) == 1:
            # Mother and father are homogenous and the            same for ofs in offspring:
                if len(ofs) == 2:
                    # Offspring is het
                    num_errors += 1
                    num_ofs_problems += 1
                elif len(ofs.intersection(mother)) == 0:
                    # Offspring is homo, but opposite from parents
                    num_errors += 2
                    num_ofs_problems += 1
        elif len(mother) == 1 and len(father) == 1:
            # Mother and father are homo and different
            for ofs in offspring:
                if len(ofs) == 1:
                    # Homo, should be het
                    num_errors += 1
                    num_ofs_problems += 1
        elif len(mother) == 2 and len(father) == 2:
            # Both are het, individual offspring can be anything
            pass
        else:
            # One is het, the other is homo
            homo = mother if len(mother) == 1 else father
            for ofs in offspring:
                if len(ofs) == 1 and ofs.intersection(homo):
                    # homo, but not including the allele from parent that is homo
                    num_errors += 1
                    num_ofs_problems += 1
        return num_errors, num_ofs_problems
    

我们将在下一个食谱中讨论这个问题,使用孟德尔误差信息进行质量控制

  1. 现在,我们定义一个支持生成器和函数,用于选择可接受的位置并累积基本数据:

    def acceptable_position_to_genotype():
        for i, genotype in enumerate(calldata_genotype):
            if is_snp[i] and num_alleles[i] == 2:
                if len(np.where(genotype == -1)[0]) > 1:
                    # Missing data
                    continue
                yield i
    def acumulate(fun):
        acumulator = {}
        for res in fun():
            if res is not None:
                acumulator[res[0]] = res[1]
        return acumulator
    
  2. 现在,我们需要找到在 HDF5 文件中交叉的索引(母亲、父亲和 20 个后代):

    def get_family_indexes(samples_hdf5, cross_pd):
        offspring = []
        for i, individual in cross_pd.T.iteritems():
            index = samples_hdf5.index(individual.id)
            if individual.function == 'parent':
                if individual.sex == 'M':
                    father = index
                else:
                    mother = index
            else:
                offspring.append(index)
        return {'mother': mother, 'father': father, 'offspring': offspring}
    cross_pd = samples[samples['cross'] == 'cross-29-2']
    family_indexes = get_family_indexes(samples_hdf5, cross_pd)
    
  3. 最后,我们将实际计算孟德尔误差并将其保存到磁盘:

    mother_index = family_indexes['mother']
    father_index = family_indexes['father']
    offspring_indexes = family_indexes['offspring']
    all_errors = {}
    def get_mendelian_errors():
        for i in acceptable_position_to_genotype():
            genotype = calldata_genotype[i]
            mother = set(genotype[mother_index])
            father = set(genotype[father_index])
            offspring = [set(genotype[ofs_index]) for ofs_index in offspring_indexes]
            my_mendelian_errors = compute_mendelian_errors(mother, father, offspring)
            yield POS[i], my_mendelian_errors
    mendelian_errors = acumulate(get_mendelian_errors)
    pickle.dump(mendelian_errors, gzip.open('mendelian_errors.pickle.gz', 'wb'))
    
  4. 我们现在将生成一个高效的带注释和孟德尔误差信息的 NumPy 数组:

    ordered_positions = sorted(mendelian_errors.keys())
    ordered_features = sorted(my_features.keys())
    num_features = len(ordered_features)
    feature_fit = np.empty((len(ordered_positions), len(my_features) + 2), dtype=float)
    for column, feature in enumerate(ordered_features):  # 'Strange' order
        print(feature)
        current_hdf_row = 0
        for row, genomic_position in enumerate(ordered_positions):
            while POS[current_hdf_row] < genomic_position:
                current_hdf_row +=1
            feature_fit[row, column] = my_features[feature][current_hdf_row]
    for row, genomic_position in enumerate(ordered_positions):
        feature_fit[row, num_features] = genomic_position
        feature_fit[row, num_features + 1] = 1 if mendelian_errors[genomic_position][0] > 0 else 0
    np.save(gzip.open('feature_fit.npy.gz', 'wb'), feature_fit, allow_pickle=False, fix_imports=False)
    pickle.dump(ordered_features, open('ordered_features', 'wb'))
    

在这段代码中埋藏着整个章节中最重要的决定之一:我们如何权衡孟德尔误差?在我们的案例中,如果有任何误差,我们只存储 1,如果没有误差,我们存储 0。另一种选择是计数错误的数量——因为我们有最多 20 个后代,这将需要一些复杂的统计分析,而我们这里不进行这种分析。

  1. 转换思路,现在让我们从染色体臂 2L 提取一些信息:

    h5_2L = h5py.File('ag1000g.crosses.phase1.ar3sites.2L.h5', 'r')
    samples_hdf5 = list(map(lambda sample: sample.decode('utf-8'), h5_2L['/2L/samples']))
    calldata_DP = h5_2L['/2L/calldata/DP']
    POS = h5_2L['/2L/variants/POS']
    
  2. 在这里,我们只关心父母:

    def get_parent_indexes(samples_hdf5, parents_pd):
        parents = []
        for i, individual in parents_pd.T.iteritems():
            index = samples_hdf5.index(individual.id)
            parents.append(index)
        return parents
    parents_pd = samples[samples['function'] == 'parent']
    parent_indexes = get_parent_indexes(samples_hdf5, parents_pd)
    
  3. 我们提取每个父母的样本 DP:

    all_dps = []
    for i, pos in enumerate(POS):
        if random.random() > 0.01:
            continue
        pos_dp = calldata_DP[i]
        parent_pos_dp = [pos_dp[parent_index] for parent_index in parent_indexes]
        all_dps.append(parent_pos_dp + [pos])
    all_dps = np.array(all_dps)
    np.save(gzip.open('DP_2L.npy.gz', 'wb'), all_dps, allow_pickle=False, fix_imports=False)
    

现在,我们已经为本章的分析准备好了数据集。

使用孟德尔误差信息进行质量控制

那么,如何使用孟德尔遗传规则推断调用质量呢?让我们看看父母不同基因型配置的预期结果:

  • 对于某个潜在的双等位基因 SNP,如果母亲是 AA 且父亲也是 AA,则所有后代将是 AA。

  • 如果母亲是 AA,父亲是 TT,则所有后代必须是杂合子(AT)。他们总是从母亲那里得到一个 A,总是从父亲那里得到一个 T。

  • 如果母亲是 AA,父亲是 AT,则后代可以是 AA 或 AT。他们总是从母亲那里得到一个 A,但可以从父亲那里得到一个 A 或一个 T。

  • 如果母亲和父亲都是杂合子(AT),则后代可以是任何基因型。从理论上讲,在这种情况下我们无法做太多。

实际上,我们可以忽略突变,这是在大多数真核生物中都可以安全做到的。从我们的角度来看,突变(噪声)的数量比我们正在寻找的信号低几个数量级。

在这个例子中,我们将进行一个小的理论研究,分析分布和孟德尔错误,并进一步处理数据以便下游分析。这相关的笔记本文件是 Chapter04/Mendel.py

如何做…

  1. 我们需要几个导入:

    import random
    import matplotlib.pyplot as plt
    
  2. 在进行任何经验分析之前,让我们尝试理解在母亲是 AA 且父亲是 AT 的情况下我们可以提取什么信息。我们来回答这个问题,如果我们有 20 个后代,所有后代为杂合子的概率是多少?

    num_sims = 100000
    num_ofs = 20
    num_hets_AA_AT = []
    for sim in range(num_sims):
        sim_hets = 0
        for ofs in range(20):
            sim_hets += 1 if random.choice([0, 1]) == 1 else 0
        num_hets_AA_AT.append(sim_hets)
    
    fig, ax = plt.subplots(1,1, figsize=(16,9))
    ax.hist(num_hets_AA_AT, bins=range(20))
    print(len([num_hets for num_hets in num_hets_AA_AT if num_hets==20]))
    

我们得到以下输出:

图 4.1 - 来自 100,000 次模拟的结果:在母亲是 AA 且父亲是杂合子的情况下,后代在某些基因座上为杂合子的数量

图 4.1 - 来自 100,000 次模拟的结果:在母亲是 AA 且父亲是杂合子的情况下,后代在某些基因座上为杂合子的数量

在这里,我们进行了 100,000 次模拟。就我而言(这是随机的,所以你的结果可能不同),我得到了零次模拟结果,其中所有的后代都是杂合子的。事实上,这些是带重复的排列,因此所有都是杂合子的概率是 或 9.5367431640625e-07——并不太可能。所以,即使对于单个后代,我们可能得到 AT 或 AA,对于 20 个后代来说,它们全部都是同一种类型的概率也非常小。这就是我们可以用来更深入解释孟德尔错误的信息。

  1. 让我们重复一下母亲和父亲都为 AT 的分析:

    num_AAs_AT_AT = []
    num_hets_AT_AT = []
    for sim in range(num_sims):
        sim_AAs = 0
        sim_hets = 0
        for ofs in range(20):
            derived_cnt = sum(random.choices([0, 1], k=2))
            sim_AAs += 1 if derived_cnt == 0 else 0
            sim_hets += 1 if derived_cnt == 1 else 0
        num_AAs_AT_AT.append(sim_AAs)
        num_hets_AT_AT.append(sim_hets)
    fig, ax = plt.subplots(1,1, figsize=(16,9))
    ax.hist([num_hets_AT_AT, num_AAs_AT_AT], histtype='step', fill=False, bins=range(20), label=['het', 'AA'])
    plt.legend()
    

输出如下:

图 4.2 - 来自 100,000 次模拟的结果:在母亲和父亲都是杂合子的情况下,后代在某个基因座上为 AA 或杂合子的数量

图 4.2 - 来自 100,000 次模拟的结果:在母亲和父亲都是杂合子的情况下,后代在某个基因座上为 AA 或杂合子的数量

在这种情况下,我们也有带重复的排列,但我们有四个可能的值,而不是两个:AA、AT、TA 和 TT。结果是所有个体为 AT 的概率相同:9.5367431640625e-07。对于所有个体都是同型合子的情况(所有为 TT 或者所有为 AA),情况更糟(实际上是两倍糟糕)。

  1. 好的,在这个概率性的序言之后,让我们开始更多数据处理的工作。我们首先要做的是检查我们有多少个错误。让我们从前一个例子中加载数据:

    import gzip
    import pickle
    import random
    import numpy as np
    mendelian_errors = pickle.load(gzip.open('mendelian_errors.pickle.gz', 'rb'))
    feature_fit = np.load(gzip.open('feature_fit.npy.gz', 'rb'))
    ordered_features = np.load(open('ordered_features', 'rb'))
    num_features = len(ordered_features)
    
  2. 让我们看看我们有多少个错误:

    print(len(mendelian_errors), len(list(filter(lambda x: x[0] > 0,mendelian_errors.values()))))
    

输出如下:

(10905732, 541688)

并不是所有的调用都有孟德尔错误——只有大约 5%,很好。

  1. 让我们创建一个平衡集,其中大约一半的集合有错误。为此,我们将随机丢弃大量正常调用。首先,我们计算错误的比例:

    total_observations = len(mendelian_errors)
    error_observations = len(list(filter(lambda x: x[0] > 0,mendelian_errors.values())))
    ok_observations = total_observations - error_observations
    fraction_errors = error_observations/total_observations
    print (total_observations, ok_observations, error_observations, 100*fraction_errors)
    del mendelian_errors
    
  2. 我们使用这些信息来获取一组被接受的条目:所有错误和一个大致相等数量的正常调用。我们在最后打印条目数量(这将随着 OK 列表的随机性而变化):

    prob_ok_choice = error_observations / ok_observations
    def accept_entry(row):
        if row[-1] == 1:
            return True
        return random.random() <= prob_ok_choice
    accept_entry_v = np.vectorize(accept_entry, signature='(i)->()')
    accepted_entries = accept_entry_v(feature_fit)
    balanced_fit = feature_fit[accepted_entries]
    del feature_fit
    balanced_fit.shape
    len([x for x in balanced_fit if x[-1] == 1]), len([x for x in balanced_fit if x[-1] == 0])
    
  3. 最后,我们保存它:

    np.save(gzip.open('balanced_fit.npy.gz', 'wb'), balanced_fit, allow_pickle=False, fix_imports=False)
    

还有更多……

关于孟德尔错误及其对成本函数的影响,让我们思考以下情况:母亲是 AA,父亲是 AT,所有后代都是 AA。这是否意味着父亲的基因型判断错误,或者我们未能检测到一些杂合的后代?从这个推理来看,可能是父亲的基因型判断错误。这在一些更精细的孟德尔错误估计函数中有影响:让几个后代错误比仅仅一个样本(父亲)错误可能更有成本。在这种情况下,你可能会认为这很简单(没有杂合子后代的概率很低,所以可能是父亲的错误),但如果有 18 个后代是 AA,2 个是 AT,那是否还能算“简单”呢?这不仅仅是一个理论问题,因为它会严重影响成本函数的设计。

我们在之前的食谱中的函数,为分析准备数据集,虽然很简单,但足够满足我们进一步获得有趣结果所需的精度。

使用标准统计方法探索数据

现在我们有了孟德尔错误分析的见解,让我们探索数据,以便获得更多可能帮助我们更好地过滤数据的见解。你可以在Chapter04/Exploration.py中找到此内容。

如何做到……

  1. 我们像往常一样,先导入必要的库:

    import gzip
    import pickle
    import random
    import numpy as np
    import matplotlib.pyplot as plt
    import pandas as pd
    from pandas.plotting import scatter_matrix
    
  2. 然后我们加载数据。我们将使用 pandas 来导航:

    fit = np.load(gzip.open('balanced_fit.npy.gz', 'rb'))
    ordered_features = np.load(open('ordered_features', 'rb'))
    num_features = len(ordered_features)
    fit_df = pd.DataFrame(fit, columns=ordered_features + ['pos', 'error'])
    num_samples = 80
    del fit
    
  3. 让我们让 pandas 显示所有注释的直方图:

    fig,ax = plt.subplots(figsize=(16,9))
    fit_df.hist(column=ordered_features, ax=ax)
    

生成的直方图如下:

图 4.3 - 数据集中所有注释的直方图,错误约占 50%

图 4.3 - 数据集中所有注释的直方图,错误约占 50%

  1. 对于某些注释,我们没有得到有趣的信息。我们可以尝试放大,举个例子,使用 DP:

    fit_df['MeanDP'] = fit_df['DP'] / 80
    fig, ax = plt.subplots()
    _ = ax.hist(fit_df[fit_df['MeanDP']<50]['MeanDP'], bins=100)
    

图 4.4 - 放大显示 DP 相关兴趣区域的直方图

图 4.4 - 放大显示 DP 相关兴趣区域的直方图

实际上,我们将 DP 除以样本数,以便得到一个更有意义的数字。

  1. 我们将把数据集分为两部分,一部分用于错误,另一部分用于没有孟德尔错误的位置:

    errors_df = fit_df[fit_df['error'] == 1]
    ok_df = fit_df[fit_df['error'] == 0]
    
  2. 让我们看一下 QUAL 并以 0.005 为分割点,检查我们如何得到错误和正确调用的分割:

    ok_qual_above_df = ok_df[ok_df['QUAL']>0.005]
    errors_qual_above_df = errors_df[errors_df['QUAL']>0.005]
    print(ok_df.size, errors_df.size, ok_qual_above_df.size, errors_qual_above_df.size)
    print(ok_qual_above_df.size / ok_df.size, errors_qual_above_df.size / errors_df.size)
    

结果如下:

6507972 6500256 484932 6114096
0.07451353509203788 0.9405931089483245

显然,['QUAL']>0.005产生了很多错误,而没有产生很多正确的位置。这是积极的,因为我们有一些希望通过过滤来处理这些错误。

  1. 对 QD 做同样的处理:

    ok_qd_above_df = ok_df[ok_df['QD']>0.05]
    errors_qd_above_df = errors_df[errors_df['QD']>0.05]
    print(ok_df.size, errors_df.size, ok_qd_above_df.size, errors_qd_above_df.size)
    print(ok_qd_above_df.size / ok_df.size, errors_qd_above_df.size / errors_df.size)
    

再次,我们得到了有趣的结果:

6507972 6500256 460296 5760288
0.07072802402960554 0.8861632526472804
  1. 让我们选择一个错误较少的区域,研究注释之间的关系。我们将成对绘制注释:

    not_bad_area_errors_df = errors_df[(errors_df['QUAL']<0.005)&(errors_df['QD']<0.05)]
    _ = scatter_matrix(not_bad_area_errors_df[['FS', 'ReadPosRankSum', 'MQ', 'HRun']], diagonal='kde', figsize=(16, 9), alpha=0.02)
    

前面的代码生成了以下输出:

图 4.5 - 搜索空间区域的错误注释散点矩阵

图 4.5 - 搜索空间区域的错误注释散点矩阵

  1. 现在对正确的调用做相同的处理:

    not_bad_area_ok_df = ok_df[(ok_df['QUAL']<0.005)&(ok_df['QD']<0.05)]
    _ = scatter_matrix(not_bad_area_ok_df[['FS', 'ReadPosRankSum', 'MQ', 'HRun']], diagonal='kde', figsize=(16, 9), alpha=0.02)
    

输出结果如下:

图 4.6 - 搜索空间区域内良好标记的注释散点矩阵

图 4.6 - 搜索空间区域内良好标记的注释散点矩阵

  1. 最后,让我们看看我们的规则在完整数据集上的表现(记住,我们使用的数据集大约由 50%的错误和 50%的正确标记组成):

    all_fit_df = pd.DataFrame(np.load(gzip.open('feature_fit.npy.gz', 'rb')), columns=ordered_features + ['pos', 'error'])
    potentially_good_corner_df = all_fit_df[(all_fit_df['QUAL']<0.005)&(all_fit_df['QD']<0.05)]
    all_errors_df=all_fit_df[all_fit_df['error'] == 1]
    print(len(all_fit_df), len(all_errors_df), len(all_errors_df) / len(all_fit_df))
    

我们得到如下结果:

10905732 541688 0.04967002673456491

让我们记住,我们的完整数据集中大约有 1090 万个标记,误差大约为 5%。

  1. 让我们获取一些关于我们good_corner的统计数据:

    potentially_good_corner_errors_df = potentially_good_corner_df[potentially_good_corner_df['error'] == 1]
    print(len(potentially_good_corner_df), len(potentially_good_corner_errors_df), len(potentially_good_corner_errors_df) / len(potentially_good_corner_df))
    print(len(potentially_good_corner_df)/len(all_fit_df))
    

输出结果如下:

9625754 32180 0.0033431147315836243
0.8826325458942141

所以,我们将误差率从 5%降低到了 0.33%,同时标记数量仅减少到了 960 万个。

还有更多……

从 5%的误差减少到 0.3%,同时失去 12%的标记,这样的变化是好还是坏呢?嗯,这取决于你接下来想要做什么样的分析。也许你的方法能抵御标记丢失,但不太容忍错误,如果是这种情况,这样的变化可能会有帮助。但如果情况相反,也许即使数据集错误更多,你也更倾向于保留完整的数据集。如果你使用不同的方法,可能会根据方法的不同而使用不同的数据集。在这个疟蚊数据集的具体案例中,数据量非常大,因此减少数据集大小对几乎所有情况都没有问题。但如果标记数量较少,你需要根据标记和质量来评估你的需求。

从测序注释中寻找基因组特征

我们将以一个简单的步骤总结这一章及本书内容,表明有时你可以从简单的、意外的结果中学到重要的东西,而表面上的质量问题可能掩盖了重要的生物学问题。

我们将绘制读取深度——DP——在染色体臂 2L 上所有交叉父本的分布情况。此步骤的代码可以在 Chapter04/2L.py 中找到。

如何做……

我们将从以下步骤开始:

  1. 让我们从常规导入开始:

    from collections import defaultdict
    import gzip
    import numpy as np
    import matplotlib.pylab as plt
    
  2. 让我们加载在第一步中保存的数据:

    num_parents = 8
    dp_2L = np.load(gzip.open('DP_2L.npy.gz', 'rb'))
    print(dp_2L.shape)
    
  3. 现在让我们打印整个染色体臂的中位 DP,以及其中部的一部分数据,针对所有父本:

    for i in range(num_parents):
        print(np.median(dp_2L[:,i]), np.median(dp_2L[50000:150000,i]))
    

输出结果如下:

17.0 14.0
23.0 22.0
31.0 29.0
28.0 24.0
32.0 27.0
31.0 31.0
25.0 24.0
24.0 20.0

有趣的是,整个染色体的中位数有时并不适用于中间的那个大区域,所以我们需要进一步挖掘。

  1. 我们将打印染色体臂上 200,000 kb 窗口的中位 DP。让我们从窗口代码开始:

    window_size = 200000
    parent_DP_windows = [defaultdict(list) for i in range(num_parents)]
    def insert_in_window(row):
        for parent in range(num_parents):
            parent_DP_windows[parent][row[-1] // window_size].append(row[parent])
    insert_in_window_v = np.vectorize(insert_in_window, signature='(n)->()')
    _ = insert_in_window_v(dp_2L)
    
  2. 让我们绘制它:

    fig, axs = plt.subplots(2, num_parents // 2, figsize=(16, 9), sharex=True, sharey=True, squeeze=True)
    for parent in range(num_parents):
        ax = axs[parent // 4][parent % 4]
        parent_data = parent_DP_windows[parent]
        ax.set_ylim(10, 40)
        ax.plot(*zip(*[(win*window_size, np.mean(lst)) for win, lst in parent_data.items()]), '.')
    
  3. 以下是绘制的结果:

图 4.7 - 所有父本数据集在染色体臂 2L 上每个窗口的中位 DP

图 4.7 - 所有父本数据集在染色体臂 2L 上每个窗口的中位 DP

你会注意到,在某些蚊子样本中,例如第一列和最后一列,染色体臂中间有明显的 DP 下降。在某些样本中,比如第三列的样本,下降程度较轻——不那么明显。对于第二列的底部父本样本,根本没有下降。

还有更多……

前述模式有一个生物学原因,最终对测序产生影响:按蚊可能在 2L 臂中间有一个大的染色体倒位。与用于做调用的参考基因组不同的核型,由于进化分化,较难进行调用。这导致该区域的测序读取数量较少。这在这种物种中特别明显,但你可能会期望在其他生物中也出现类似特征。

一个更广为人知的案例是 n,此时你可以预期在整个基因组中看到 n 倍于中位数的 DP。

但在一般情况下,保持警惕并留意分析中的 异常 结果是个好主意。有时候,这正是一个有趣生物学特征的标志,就像这里一样。要么这就是指向一个错误的信号:例如,主成分分析PCA)可以用来发现标签错误的样本(因为它们可能会聚集在错误的组中)。

使用 QIIME 2 Python API 进行宏基因组学分析

Wikipedia 表示,宏基因组学是直接从环境样本中回收遗传物质的研究。注意,这里的“环境”应广泛理解:在我们的例子中,我们将处理肠道微生物组,研究的是儿童肠道问题中的粪便微生物组移植。该研究是 QIIME 2 的其中一个教程,QIIME 2 是最广泛使用的宏基因组数据分析应用之一。QIIME 2 有多个接口:图形用户界面(GUI)、命令行界面和一个称为 Artifact API 的 Python API。

Tomasz Kościółek 提供了一个出色的 Artifact API 使用教程,基于 QIIME 2 最成熟的(客户端基础的,而非基于 Artifact 的)教程,即 “Moving Pictures” 教程(nbviewer.jupyter.org/gist/tkosciol/29de5198a4be81559a075756c2490fde)。在这里,我们将创建一个 Python 版本的粪便微生物群移植研究,正如客户端接口一样,详细内容可参见 docs.qiime2.org/2022.2/tutorials/fmt/。你应该熟悉它,因为我们不会在这里深入探讨生物学的细节。我走的路线比 Tomasz 更为复杂:这将帮助你更好地了解 QIIME 2 Python 的内部结构。获得这些经验后,你可能更倾向于按照 Tomasz 的路线,而非我的路线。然而,你在这里获得的经验将使你在使用 QIIME 的内部功能时更加舒适和自信。

准备开始

这个设置稍微复杂一些。我们将需要创建一个 conda 环境,将 QIIME 2 的软件包与其他应用程序的软件包分开。你需要遵循的步骤很简单。

在 OS X 上,使用以下代码创建一个新的 conda 环境:

wget wget https://data.qiime2.org/distro/core/qiime2-2022.2-py38-osx-conda.yml
conda env create -n qiime2-2022.2 --file qiime2-2022.2-py38-osx-conda.yml

在 Linux 上,使用以下代码创建环境:

wget wget https://data.qiime2.org/distro/core/qiime2-2022.2-py38-linux-conda.yml
conda env create -n qiime2-2022.2 --file qiime2-2022.2-py38-linux-conda.yml

如果这些指令不起作用,请查看 QIIME 2 网站上的更新版本(docs.qiime2.org/2022.2/install/native)。QIIME 2 会定期更新。

在这个阶段,你需要通过使用source activate qiime2-2022.2进入 QIIME 2 的conda环境。如果你想回到标准的conda环境,可以使用source deactivate。我们将安装jupyter labjupytext

conda install jupyterlab jupytext

你可能想要在 QIIME 2 的环境中使用conda install安装其他你想要的包。

为了准备 Jupyter 执行,你应该安装 QIIME 2 扩展,方法如下:

jupyter serverextension enable --py qiime2 --sys-prefix

提示

该扩展具有高度交互性,允许你从不同的视角查看数据,这些视角在本书中无法捕捉。缺点是它不能在nbviewer中工作(某些单元格的输出在静态查看器中无法显示)。记得与扩展中的输出进行交互,因为许多输出是动态的。

你现在可以启动 Jupyter。Notebook 可以在Chapter4/QIIME2_Metagenomics.py文件中找到。

警告

由于 QIIME 的包安装具有流动性,我们没有为其提供 Docker 环境。这意味着如果你是通过我们的 Docker 安装工作,你将需要下载食谱并手动安装这些包。

你可以找到获取 Notebook 文件和 QIIME 2 教程数据的说明。

如何操作...

让我们看一下接下来的步骤:

  1. 让我们首先检查一下有哪些插件可用:

    import pandas as pd
    from qiime2.metadata.metadata import Metadata
    from qiime2.metadata.metadata import CategoricalMetadataColumn
    from qiime2.sdk import Artifact
    from qiime2.sdk import PluginManager
    from qiime2.sdk import Result
    pm = PluginManager()
    demux_plugin = pm.plugins['demux']
    #demux_emp_single = demux_plugin.actions['emp_single']
    demux_summarize = demux_plugin.actions['summarize']
    print(pm.plugins)
    

我们还在访问解混插件及其摘要操作。

  1. 让我们来看看摘要操作,即inputsoutputsparameters

    print(demux_summarize.description)
    demux_summarize_signature = demux_summarize.signature
    print(demux_summarize_signature.inputs)
    print(demux_summarize_signature.parameters)
    print(demux_summarize_signature.outputs)
    

输出将如下所示:

Summarize counts per sample for all samples, and generate interactive positional quality plots based on `n` randomly selected sequences.
 OrderedDict([('data', ParameterSpec(qiime_type=SampleData[JoinedSequencesWithQuality | PairedEndSequencesWithQuality | SequencesWithQuality], view_type=<class 'q2_demux._summarize._visualizer._PlotQualView'>, default=NOVALUE, description='The demultiplexed sequences to be summarized.'))])
 OrderedDict([('n', ParameterSpec(qiime_type=Int, view_type=<class 'int'>, default=10000, description='The number of sequences that should be selected at random for quality score plots. The quality plots will present the average positional qualities across all of the sequences selected. If input sequences are paired end, plots will be generated for both forward and reverse reads for the same `n` sequences.'))])
 OrderedDict([('visualization', ParameterSpec(qiime_type=Visualization, view_type=None, default=NOVALUE, description=NOVALUE))])
  1. 现在,我们将加载第一个数据集,对其进行解混,并可视化一些解混统计数据:

    seqs1 = Result.load('fmt-tutorial-demux-1-10p.qza')
    sum_data1 = demux_summarize(seqs1)
    sum_data1.visualization
    

这是来自 QIIME 扩展为 Jupyter 提供的输出的一部分:

图 4.8 - QIIME 2 扩展为 Jupyter 提供的输出部分

图 4.8 - QIIME 2 扩展为 Jupyter 提供的输出部分

记住,扩展是迭代的,并且提供的信息比仅仅这张图表多得多。

提示

本食谱的原始数据是以 QIIME 2 格式提供的。显然,你将拥有自己原始数据的其他格式(可能是 FASTQ 格式)—请参见还有更多...部分,了解如何加载标准格式。

QIIME 2 的.qza.qzv格式只是压缩文件。你可以通过unzip查看其内容。

图表将类似于 QIIME CLI 教程中的图表,但务必检查我们输出的交互质量图。

  1. 让我们对第二个数据集做相同的操作:

    seqs2 = Result.load('fmt-tutorial-demux-2-10p.qza')
    sum_data2 = demux_summarize(seqs2)
    sum_data2.visualization
    
  2. 让我们使用 DADA2(github.com/benjjneb/dada2)插件进行质量控制:

    dada2_plugin = pm.plugins['dada2']
    dada2_denoise_single = dada2_plugin.actions['denoise_single']
    qual_control1 = dada2_denoise_single(demultiplexed_seqs=seqs1,
                                        trunc_len=150, trim_left=13)
    qual_control2 = dada2_denoise_single(demultiplexed_seqs=seqs2,
                                        trunc_len=150, trim_left=13)
    
  3. 让我们从去噪(第一组)中提取一些统计数据:

    metadata_plugin = pm.plugins['metadata']
    metadata_tabulate = metadata_plugin.actions['tabulate']
    stats_meta1 = metadata_tabulate(input=qual_control1.denoising_stats.view(Metadata))
    stats_meta1.visualization
    

同样,结果可以在 QIIME 2 命令行版本的教程中找到。

  1. 现在,让我们对第二组数据做相同的操作:

    stats_meta2 = metadata_tabulate(input=qual_control2.denoising_stats.view(Metadata))
    stats_meta2.visualization
    
  2. 现在,合并去噪后的数据:

    ft_plugin = pm.plugins['feature-table']
    ft_merge = ft_plugin.actions['merge']
    ft_merge_seqs = ft_plugin.actions['merge_seqs']
    ft_summarize = ft_plugin.actions['summarize']
    ft_tab_seqs = ft_plugin.actions['tabulate_seqs']
    table_merge = ft_merge(tables=[qual_control1.table, qual_control2.table])
    seqs_merge = ft_merge_seqs(data=[qual_control1.representative_sequences, qual_control2.representative_sequences])
    
  3. 然后,从合并结果中收集一些质量统计数据:

    ft_sum = ft_summarize(table=table_merge.merged_table)
    ft_sum.visualization
    
  4. 最后,让我们获取一些关于合并序列的信息:

    tab_seqs = ft_tab_seqs(data=seqs_merge.merged_data)
    tab_seqs.visualization
    

还有更多内容...

上面的代码没有展示如何导入数据。实际代码会根据情况有所不同(单端数据、双端数据,或已经解多重条形码的数据),但对于主要的 QIIME 2 教程《移动图像》,假设你已经将单端、未解多重条形码的数据和条形码下载到名为data的目录中,你可以执行以下操作:

data_type = 'EMPSingleEndSequences'
conv = Artifact.import_data(data_type, 'data')
conv.save('out.qza')

如上面的代码所述,如果你在 GitHub 上查看这个笔记本,静态的nbviewer系统将无法正确渲染笔记本(你需要自己运行它)。这远非完美;它不具备交互性,质量也不是很好,但至少它能让你在不运行代码的情况下大致了解输出结果。

第六章:处理基因组

计算生物学中的许多任务依赖于参考基因组的存在。如果你正在进行序列比对、寻找基因或研究种群遗传学,你将直接或间接使用参考基因组。在本章中,我们将开发一些处理参考基因组的技术,解决不同质量的参考基因组问题,这些基因组的质量可能从高质量(在这里,高质量仅指基因组组装的状态,这是本章的重点),如人类基因组,到存在问题的非模式物种基因组。我们还将学习如何处理基因组注释(处理能够指引我们发现基因组中有趣特征的数据库),并使用注释信息提取序列数据。我们还将尝试跨物种寻找基因同源物。最后,我们将访问基因本体论GO)数据库。

在本章中,我们将涵盖以下内容:

  • 处理高质量参考基因组

  • 处理低质量参考基因组

  • 遍历基因组注释

  • 使用注释从参考基因组中提取基因

  • 使用 Ensembl REST API 查找同源基因

  • 从 Ensembl 获取基因本体信息

技术要求

如果你通过 Docker 运行本章内容,你可以使用tiagoantao/bioinformatics_genomes镜像。如果你使用 Anaconda,本章所需的软件将在每个相关部分介绍。

处理高质量参考基因组

在本节中,你将学习一些操作参考基因组的通用技术。作为一个示例,我们将研究恶性疟原虫的 GC 含量——即基因组中以鸟嘌呤-胞嘧啶为基础的部分,这是导致疟疾的最重要寄生虫物种。参考基因组通常以 FASTA 文件的形式提供。

准备工作

生物体基因组的大小差异很大,从像 HIV 这样的病毒(其基因组为 9.7 kbp)到像大肠杆菌这样的细菌,再到像恶性疟原虫这样的原生动物(其基因组跨越 14 条染色体、线粒体和顶体,大小为 22 Mbp),再到果蝇,具有三对常染色体、线粒体和 X/Y 性染色体,再到人类,其基因组由 22 对常染色体、X/Y 染色体和线粒体组成,总大小为 3 Gbp,一直到日本巴黎,一种植物,基因组大小为 150 Gbp。在这个过程中,你会遇到不同的倍性和性染色体组织。

提示

正如你所看到的,不同的生物体有非常不同的基因组大小。这个差异可以达到几个数量级。这对你的编程风格有重大影响。处理一个大型基因组将要求你更加节省内存。不幸的是,更大的基因组将从更高效的编程技术中受益(因为你需要分析的数据更多);这些是相互矛盾的要求。一般来说,对于较大的基因组,你必须更加小心地处理效率(无论是速度还是内存)。

为了让这个方法不那么繁琐,我们将使用恶性疟原虫的一个小型真核基因组。这个基因组仍然具有大基因组的一些典型特征(例如,多个染色体)。因此,它在复杂性和大小之间是一个很好的折衷。请注意,对于像恶性疟原虫这样大小的基因组,可以通过将整个基因组加载到内存中来执行许多操作。然而,我们选择了一种可以应用于更大基因组(例如,哺乳动物)的编程风格,这样你可以以更通用的方式使用这个方法,但如果是像这种小型基因组,也可以使用更依赖内存的方式。

我们将使用 Biopython,这是你在第一章Python 与相关软件生态中安装的。如往常一样,这个方法可以在本书的 Jupyter 笔记本中找到,路径为Chapter05/Reference_Genome.py,在本书的代码包中。我们需要下载参考基因组——你可以在上述笔记本中找到最新的下载地址。为了生成本方法最后的图表,我们还需要reportlab

conda install -c bioconda reportlab

现在,我们准备开始。

如何操作...

按照以下步骤进行:

  1. 我们将首先检查参考基因组 FASTA 文件中所有序列的描述:

    from Bio import SeqIO
    genome_name = 'PlasmoDB-9.3_Pfalciparum3D7_Genome.fasta'
    recs = SeqIO.parse(genome_name, 'fasta')
    for rec in recs:
        print(rec.description)
    

这段代码应该很熟悉,来自上一章,第三章下一代测序。让我们来看一下部分输出:

图 5.1 – 显示恶性疟原虫参考基因组的 FASTA 描述的输出

不同的基因组参考将有不同的描述行,但它们通常包含重要信息。在这个示例中,你可以看到我们有染色体、线粒体和顶体。我们还可以查看染色体的大小,但我们将使用序列长度中的值。

  1. 让我们解析描述行,以提取染色体编号。我们将从序列中获取染色体大小,并基于窗口计算每个染色体的GC含量:

    from Bio import SeqUtils
    recs = SeqIO.parse(genome_name, 'fasta')
    chrom_sizes = {}
    chrom_GC = {}
    block_size = 50000
    min_GC = 100.0
    max_GC = 0.0
    for rec in recs:
        if rec.description.find('SO=chromosome') == -1:
            continue
        chrom = int(rec.description.split('_')[1])
        chrom_GC[chrom] = []
        size = len(rec.seq)
        chrom_sizes[chrom] = size
        num_blocks = size // block_size + 1
        for block in range(num_blocks):
            start = block_size * block
            if block == num_blocks - 1:
                end = size
            else:
                end = block_size + start + 1
            block_seq = rec.seq[start:end]
            block_GC = SeqUtils.GC(block_seq)
            if block_GC < min_GC:
                min_GC = block_GC
            if block_GC > max_GC:
                max_GC = block_GC
            chrom_GC[chrom].append(block_GC)
    print(min_GC, max_GC)
    

在这里,我们对所有染色体进行了窗口分析,类似于我们在第三章中所做的,下一代测序。我们从定义一个 50 kbp 的窗口大小开始。这对于Plasmodium falciparum来说是合适的(你可以自由调整其大小),但对于那些染色体大小差异数量级较大的基因组,你会想考虑其他的数值。

注意,我们正在重新读取文件。由于基因组如此之小,实际上在步骤 1中可以将整个基因组加载到内存中。对于小型基因组来说,尝试这种编程风格是可行的——它更快!然而,我们的代码是为了可以在更大的基因组上复用而设计的。

  1. 注意,在for循环中,我们通过解析SO条目来忽略线粒体和顶体。chrom_sizes字典将维护染色体的大小。

chrom_GC字典是我们最有趣的数据结构,将包含每个 50 kbp 窗口中GC含量的一个分数的列表。所以,染色体 1 的大小为 640,851 bp,它会有 14 个条目,因为该染色体的大小为 14 个 50 kbp 的块。

注意疟原虫Plasmodium falciparum)基因组的两个不寻常特征:该基因组非常富含 AT,即 GC 贫乏。因此,你得到的数字会非常低。此外,染色体是按大小排序的(这很常见),但从最小的大小开始。通常的约定是从最大的大小开始(比如在人类基因组中)。

  1. 现在,让我们创建一个GC分布的基因组图。我们将使用蓝色的不同色调表示GC含量。然而,对于高离群值,我们将使用红色的不同色调。对于低离群值,我们将使用黄色的不同色调:

    from reportlab.lib import colors
    from reportlab.lib.units import cm
    from Bio.Graphics import BasicChromosome
    chroms = list(chrom_sizes.keys())
    chroms.sort()
    biggest_chrom = max(chrom_sizes.values())
    my_genome = BasicChromosome.Organism(output_format="png")
    my_genome.page_size = (29.7*cm, 21*cm)
    telomere_length = 10
    bottom_GC = 17.5
    top_GC = 22.0
    for chrom in chroms:
        chrom_size = chrom_sizes[chrom]
        chrom_representation = BasicChromosome.Chromosome ('Cr %d' % chrom)
        chrom_representation.scale_num = biggest_chrom
        tel = BasicChromosome.TelomereSegment()
        tel.scale = telomere_length
        chrom_representation.add(tel)
        num_blocks = len(chrom_GC[chrom])
        for block, gc in enumerate(chrom_GC[chrom]):
            my_GC = chrom_GC[chrom][block]
            body = BasicChromosome.ChromosomeSegment()
            if my_GC > top_GC:
                body.fill_color = colors.Color(1, 0, 0)
            elif my_GC < bottom_GC:
                body.fill_color = colors.Color(1, 1, 0)
            else:
                my_color = (my_GC - bottom_GC) / (top_GC -bottom_GC)
                body.fill_color = colors.Color(my_color,my_color, 1)
            if block < num_blocks - 1:
                body.scale = block_size
            else:
                body.scale = chrom_size % block_size
            chrom_representation.add(body)
        tel = BasicChromosome.TelomereSegment(inverted=True)
        tel.scale = telomere_length
        chrom_representation.add(tel)
        my_genome.add(chrom_representation)
    my_genome.draw('falciparum.png', 'Plasmodium falciparum')
    

第一行将keys方法的返回值转换为一个列表。在 Python 2 中这是多余的,但在 Python 3 中并非如此,因为keys方法的返回类型是特定的dict_keys类型。

我们按顺序绘制染色体(因此需要排序)。我们需要最大的染色体的大小(在Plasmodium falciparum中为 14)来确保染色体的大小以正确的比例打印(即biggest_chrom变量)。

然后,我们创建一个 A4 大小的有机体表示,并输出为 PNG 文件。注意,我们绘制了非常小的端粒(10 bp)。这将产生一个类似矩形的染色体。你可以将端粒做得更大,给它们一个圆形的表示,或者你也可以选择使用适合你物种的端粒尺寸。

我们声明,任何GC含量低于 17.5%或高于 22.0%的都会被视为离群值。记住,对于大多数其他物种来说,这个值会更高。

然后,我们打印这些染色体:它们被端粒限制,且由 50 kbp 的染色体片段组成(最后一个片段的大小为剩余部分)。每个片段将用蓝色表示,并基于两种极值之间的线性归一化,呈现红绿成分。每个染色体片段将为 50 kbp,或者如果它是染色体的最后一个片段,可能会更小。输出结果如下图所示:

图 5.2 – 疟原虫的 14 条染色体,用 GC 含量进行颜色编码(红色表示超过 22%,黄色表示少于 17%,蓝色阴影代表这两个数值之间的线性渐变)

提示

Biopython 代码的演变发生在 Python 成为流行语言之前。过去,库的可用性非常有限。reportlab的使用大多可以视为遗留问题。我建议你只需学到足够的知识,能与 Biopython 配合使用。如果你计划学习现代的 Python 绘图库,那么标准的代表是 Matplotlib,正如我们在*第二章中所学的,《了解 NumPy、pandas、Arrow 和 Matplotlib》。替代方案包括 Bokeh、HoloViews,或 Python 版本的 ggplot(甚至更复杂的可视化替代方案,如 Mayavi、可视化工具包VTK**)甚至 Blender API)。

  1. 最后,你可以在笔记本中内嵌打印图像:

    from IPython.core.display import Image
    Image("falciparum.png")
    

就这样完成了这个方案!

还有更多……

疟原虫是一个合理的例子,它是一个基因组较小的真核生物,能够让你进行一个具有足够特征的小型数据练习,同时对于大多数真核生物仍然具有参考价值。当然,它没有性染色体(如人类的 X/Y 染色体),但这些应该容易处理,因为参考基因组并不涉及倍性问题。

疟原虫确实有线粒体,但由于篇幅限制,我们在此不讨论它。Biopython 确实具有打印圆形基因组的功能,你也可以将其用于细菌。关于细菌和病毒,这些基因组更容易处理,因为它们的大小非常小。

另见

这里有一些你可以深入了解的资源:

  • 你可以在 Ensembl 网站上找到许多模式生物的参考基因组,网址是www.ensembl.org/info/data/ftp/index.xhtml

  • 像往常一样,国家生物技术信息中心NCBI)也提供了一个庞大的基因组列表,网址是www.ncbi.nlm.nih.gov/genome/browse/

  • 有许多网站专注于单一生物体(或一组相关生物体)。除了你从Plasmodium falciparum基因组下载的 PlasmoDB(plasmodb.org/plasmo/),你还会在下一个关于病媒生物的食谱中找到 VectorBase(www.vectorbase.org/)。用于果蝇(Drosophila melanogaster)的 FlyBase(flybase.org/)也值得一提,但不要忘记搜索你感兴趣的生物体。

处理低质量的基因组参考

不幸的是,并非所有参考基因组都具备Plasmodium falciparum那样的质量。除了某些模型物种(例如人类,或常见的果蝇Drosophila melanogaster)和少数其他物种外,大多数参考基因组仍有待改进。在本食谱中,我们将学习如何处理低质量的参考基因组。

准备工作

继续围绕疟疾主题,我们将使用两种疟疾传播蚊子的参考基因组:Anopheles gambiae(这是最重要的疟疾传播者,存在于撒哈拉以南的非洲)和Anopheles atroparvus,一种欧洲的疟疾传播者(尽管欧洲已经消除了该病,但该传播者依然存在)。Anopheles gambiae基因组质量较好,大多数染色体已经被映射,尽管 Y 染色体仍需要一些工作。还有一个相当大的未知染色体,可能由 X 和 Y 染色体的部分片段以及中肠微生物群组成。该基因组中有一些位置未被注释(即,你会看到N而不是 ACTG)。Anopheles atroparvus基因组仍处于框架格式。遗憾的是,许多非模型物种的基因组都是这种情况。

请注意,我们将稍微提高难度。Anopheles基因组比Plasmodium falciparum基因组大一个数量级(但仍然比大多数哺乳动物小一个数量级)。

我们将使用你在第一章中安装的 Biopython,Python 和周边软件生态。像往常一样,本食谱可在本书的 Jupyter 笔记本中找到,路径为Chapter05/Low_Quality.py,在本书的代码包中。在笔记本的开始部分,你可以找到两个基因组的最新位置,以及下载它们的代码。

如何操作...

请按照以下步骤操作:

  1. 让我们从列出Anopheles gambiae基因组的染色体开始:

    import gzip
    from Bio import SeqIO
    gambiae_name = 'gambiae.fa.gz'
    atroparvus_name = 'atroparvus.fa.gz'
    recs = SeqIO.parse(gzip.open(gambiae_name, 'rt', encoding='utf-8'), 'fasta')
    for rec in recs:
        print(rec.description)
    

这将产生一个输出,其中包括生物体的染色体(以及一些未映射的超级重组片段,未显示):

AgamP4_2L | organism=Anopheles_gambiae_PEST | version=AgamP4 | length=49364325 | SO=chromosome
AgamP4_2R | organism=Anopheles_gambiae_PEST | version=AgamP4 | length=61545105 | SO=chromosome
AgamP4_3L | organism=Anopheles_gambiae_PEST | version=AgamP4 | length=41963435 | SO=chromosome
AgamP4_3R | organism=Anopheles_gambiae_PEST | version=AgamP4 | length=53200684 | SO=chromosome
AgamP4_X | organism=Anopheles_gambiae_PEST | version=AgamP4 | length=24393108 | SO=chromosome
AgamP4_Y_unplaced | organism=Anopheles_gambiae_PEST | version=AgamP4 | length=237045 | SO=chromosome
AgamP4_Mt | organism=Anopheles_gambiae_PEST | version=AgamP4 | length=15363 | SO=mitochondrial_chromosome

代码非常简单。我们使用gzip模块,因为较大基因组的文件通常是压缩的。我们可以看到四个染色体臂(2L2R3L3R)、线粒体(Mt)、X 染色体和 Y 染色体,后者非常小,名字几乎表明它的状态可能不佳。此外,未知的(UNKN)染色体占参考基因组的较大比例,几乎相当于一个染色体臂的大小。

不要在Anopheles atroparvus上执行此操作;否则,您将得到超过一千个条目,这归功于 scaffolding 状态。

  1. 现在,让我们检查一下未调用的位置(Ns)及其在按蚊(Anopheles gambiae)基因组中的分布:

    recs = SeqIO.parse(gzip.open(gambiae_name, 'rt', encoding='utf-8'), 'fasta')
    chrom_Ns = {}
    chrom_sizes = {}
    for rec in recs:
        if rec.description.find('supercontig') > -1:
            continue
        print(rec.description, rec.id, rec)
        chrom = rec.id.split('_')[1]
        if chrom in ['UNKN']:
            continue
        chrom_Ns[chrom] = []
        on_N = False
        curr_size = 0
        for pos, nuc in enumerate(rec.seq):
            if nuc in ['N', 'n']:
                curr_size += 1
                on_N = True
            else:
                if on_N:
                    chrom_Ns[chrom].append(curr_size)
                    curr_size = 0
                on_N = False
        if on_N:
            chrom_Ns[chrom].append(curr_size)
        chrom_sizes[chrom] = len(rec.seq)
    for chrom, Ns in chrom_Ns.items():
        size = chrom_sizes[chrom]
        if len(Ns) > 0:
            max_Ns = max(Ns)
        else:
            max_Ns = 'NA'
        print(f'{chrom} ({size}): %Ns ({round(100 * sum(Ns) / size, 1)}), num Ns: {len(Ns)}, max N: {max_Ns}')
    

上面的代码将需要一些时间来运行,请耐心等待;我们将检查所有常染色体的每个碱基对。和往常一样,我们将重新打开并重新读取文件以节省内存。

我们有两个字典:一个包含染色体大小,另一个包含 Ns 运行的大小分布。为了计算 Ns 运行,我们必须遍历所有常染色体(注意何时 N 位置开始和结束)。然后,我们必须打印 Ns 分布的基本统计信息:

2L (49364325): %Ns (1.7), num Ns: 957, max N: 28884
2R (61545105): %Ns (2.3), num Ns: 1658, max N: 36427
3L (41963435): %Ns (2.9), num Ns: 1272, max N: 31063
3R (53200684): %Ns (1.8), num Ns: 1128, max N: 24292
X (24393108): %Ns (4.1), num Ns: 1287, max N: 21132
Y (237045): %Ns (43.0), num Ns: 63, max N: 7957
Mt (15363): %Ns (0.0), num Ns: 0, max N: NA

因此,对于 2L 染色体臂(大小为 49 Mbp),1.7% 是 N 调用,并且分布在 957 个运行中。最大的运行是 28884 bp。请注意,X 染色体具有最高的 Ns 位置比例。

  1. 现在,让我们将注意力转向Anopheles Atroparvus基因组。让我们计算一下 scaffolds 的数量,并且查看 scaffold 大小的分布:

    import numpy as np
    recs = SeqIO.parse(gzip.open(atroparvus_name, 'rt', encoding='utf-8'), 'fasta')
    sizes = []
    size_N = []
    for rec in recs:
        size = len(rec.seq)
        sizes.append(size)
        count_N = 0
        for nuc in rec.seq:
            if nuc in ['n', 'N']:
                count_N += 1
        size_N.append((size, count_N / size))
    print(len(sizes), np.median(sizes), np.mean(sizes),
          max(sizes), min(sizes),
          np.percentile(sizes, 10), np.percentile(sizes, 90))
    

这段代码与我们之前看到的相似,但我们使用 NumPy 打印了更详细的统计信息,因此我们得到了以下结果:

1320 7811.5 170678.2 58369459 1004 1537.1 39644.7

因此,我们有 1371 个 scaffolds(与Anopheles gambiae基因组中的七个条目相比),中位大小为 7811.5(平均值为 17,0678.2)。最大的 scaffold 为 5.8 Mbp,最小的 scaffold 为 1004 bp。大小的第十百分位为 1537.1,而第九十百分位为 39644.7

  1. 最后,让我们绘制 scaffold 的比例 —— 即 N —— 作为其大小的函数:

    import matplotlib.pyplot as plt
    small_split = 4800
    large_split = 540000
    fig, axs = plt.subplots(1, 3, figsize=(16, 9), squeeze=False, sharey=True)
    xs, ys = zip(*[(x, 100 * y) for x, y in size_N if x <= small_split])
    axs[0, 0].plot(xs, ys, '.')
    xs, ys = zip(*[(x, 100 * y) for x, y in size_N if x > small_split and x <= large_split])
    axs[0, 1].plot(xs, ys, '.')
    axs[0, 1].set_xlim(small_split, large_split)
    xs, ys = zip(*[(x, 100 * y) for x, y in size_N if x > large_split])
    axs[0, 2].plot(xs, ys, '.')
    axs[0, 0].set_ylabel('Fraction of Ns', fontsize=12)
    axs[0, 1].set_xlabel('Contig size', fontsize=12)
    fig.suptitle('Fraction of Ns per contig size', fontsize=26)
    

上面的代码将生成如下图所示的输出,在该图中,我们将图表按 scaffold 大小分成三部分:一部分用于小于 4,800 bp 的 scaffolds,一部分用于介于 4,800 和 540,000 bp 之间的 scaffolds,另一部分用于更大的 scaffolds。小型 scaffolds 的 Ns 比例非常低(始终低于 3.5%);中型 scaffolds 的 Ns 比例有较大变异(大小范围为 0% 至超过 90%);而对于最大的 scaffolds,Ns 的变异性较小(在 0% 至 25% 之间)。

图 5.3 – 作为其大小函数的 N 片段比例

还有更多内容...

有时,参考基因组携带额外的信息。例如,按蚊基因组是软屏蔽的。这意味着对基因组进行了某些操作,以识别低复杂度区域(这些区域通常更难分析)。这种情况可以通过大写字母注释:ACTG 表示高复杂度,而 actg 表示低复杂度。

拥有大量 scaffolds 的参考基因组不仅仅是麻烦。例如,非常小的 scaffold(比如小于 2000 bp)在使用比对工具时(如Burrows-Wheeler 比对器BWA))可能会出现比对问题,尤其是在极端位置(大多数 scaffold 在极端位置会有比对问题,但如果 scaffold 较小,这些问题将占据 scaffold 更大的比例)。如果你使用这样的参考基因组进行比对,建议在比对到小 scaffold 时考虑忽略配对信息(假设你有双端读取),或者至少衡量 scaffold 大小对比对工具性能的影响。无论如何,关键在于你应该小心,因为 scaffold 的大小和数量会时不时地带来麻烦。

对于这些基因组,仅识别出完全模糊(N)。请注意,其他基因组组装可能会给出一个介于模糊和确定性ACTG)之间的中间代码。

另见

以下是一些你可以从中了解更多信息的资源:

遍历基因组注释

拥有一个基因组序列很有趣,但我们还需要从中提取特征,例如基因、外显子和编码序列。这类注释信息通常以通用特征格式GFF)和通用转移格式GTF)文件的形式提供。在本教程中,我们将以按蚊基因组的注释为例,学习如何解析和分析 GFF 文件。

准备工作

使用提供的Chapter05/Annotations.py笔记本文件,该文件包含在本书的代码包中。我们将使用的 GFF 文件的最新位置可以在笔记本顶部找到。

你需要安装gffutils

conda install -c bioconda gffutils

现在,我们准备开始了。

如何做...

按照以下步骤操作:

  1. 让我们从创建一个基于 GFF 文件的注释数据库开始,使用gffutils

    import gffutils
    import sqlite3
    try:
        db = gffutils.create_db('gambiae.gff.gz', 'ag.db')
    except sqlite3.OperationalError:
        db = gffutils.FeatureDB('ag.db')
    

gffutils库创建一个 SQLite 数据库来高效存储注释信息。在这里,我们将尝试创建数据库,如果数据库已存在,则使用现有的。这一步可能需要一些时间。

  1. 现在,让我们列出所有可用的特征类型并统计它们:

    print(list(db.featuretypes()))
    for feat_type in db.featuretypes():
        print(feat_type, db.count_features_of_type(feat_type))
    

这些特征包括 contigs、基因、外显子、转录本等等。请注意,我们将使用 gffutils 包的 featuretypes 函数。它将返回一个生成器,但我们会将其转换为列表(在这里这样做是安全的)。

  1. 让我们列出所有的 seqids:

    seqids = set()
    for e in db.all_features():
        seqids.add(e.seqid)
    for seqid in seqids:
        print(seqid)
    

这将显示所有染色体臂和性染色体、线粒体以及未知染色体的注释信息。

  1. 现在,让我们按染色体提取大量有用的信息,比如基因数量、每个基因的转录本数量、外显子数量等等:

    from collections import defaultdict
    num_mRNAs = defaultdict(int)
    num_exons = defaultdict(int)
    max_exons = 0
    max_span = 0
    for seqid in seqids:
        cnt = 0
        for gene in db.region(seqid=seqid, featuretype='protein_coding_gene'):
            cnt += 1
            span = abs(gene.start - gene.end) # strand
            if span > max_span:
                max_span = span
                max_span_gene = gene
            my_mRNAs = list(db.children(gene, featuretype='mRNA'))
            num_mRNAs[len(my_mRNAs)] += 1
            if len(my_mRNAs) == 0:
                exon_check = [gene]
            else:
                exon_check = my_mRNAs
            for check in exon_check:
                my_exons = list(db.children(check, featuretype='exon'))
                num_exons[len(my_exons)] += 1
                if len(my_exons) > max_exons:
                    max_exons = len(my_exons)
                    max_exons_gene = gene
        print(f'seqid {seqid}, number of genes {cnt}')
    print('Max number of exons: %s (%d)' % (max_exons_gene.id, max_exons))
    print('Max span: %s (%d)' % (max_span_gene.id, max_span))
    print(num_mRNAs)
    print(num_exons)
    

我们将在提取所有蛋白质编码基因的同时遍历所有 seqids(使用 region)。在每个基因中,我们统计可变转录本的数量。如果没有(注意这可能是注释问题,而不是生物学问题),我们统计外显子数(children)。如果有多个转录本,我们统计每个转录本的外显子数。我们还会考虑跨度大小,以检查跨越最大区域的基因。

我们遵循类似的步骤来查找基因和最大的外显子数。最后,我们打印一个字典,包含每个基因的可变转录本数量分布(num_mRNAs)和每个转录本的外显子数量分布(num_exons)。

还有更多...

GFF/GTF 格式有许多变种。不同的 GFF 版本和许多非官方的变体。如果可能的话,选择 GFF 版本 3。然而,残酷的事实是,你会发现处理这些文件非常困难。gffutils 库尽可能地适应了这一点。事实上,许多关于这个库的文档都是帮助你处理各种尴尬变体的(参考 pythonhosted.org/gffutils/examples.xhtml)。

使用 gffutils 也有替代方案(无论是因为你的 GFF 文件有问题,还是你不喜欢这个库的接口或它依赖 SQL 后端)。自己手动解析文件。如果你看一下格式,你会发现它并不复杂。如果你只进行一次性操作,手动解析或许足够。当然,长期来看,一次性操作往往并不是最好的选择。

另外,请注意,注释的质量往往差异很大。随着质量的提高,复杂性也会增加。只要看看人类注释,就能看到这一点的例子。可以预见,随着我们对生物体的认识不断深入,注释的质量和复杂性也会逐渐提升。

另请参见

以下是一些你可以学习更多资源:

从参考基因组中提取基因信息

在这个步骤中,我们将学习如何借助注释文件提取基因序列,并将其与参考 FASTA 文件的坐标对齐。我们将使用 Anopheles gambiae 基因组及其注释文件(如前两个步骤所示)。首先,我们将提取 电压门控钠通道VGSC)基因,该基因与抗虫剂的抗性相关。

准备开始

如果你已经按照前两个步骤操作,你就准备好了。如果没有,下载 Anopheles gambiae 的 FASTA 文件和 GTF 文件。你还需要准备 gffutils 数据库:

import gffutils
import sqlite3
try:
    db = gffutils.create_db('gambiae.gff.gz', 'ag.db')
except sqlite3.OperationalError:
    db = gffutils.FeatureDB('ag.db')

和往常一样,你将会在 Chapter05/Getting_Gene.py 笔记本文件中找到所有这些内容。

如何操作...

按照以下步骤操作:

  1. 让我们从获取基因的注释信息开始:

    import gzip
    from Bio import Seq, SeqIO
    gene_id = 'AGAP004707'
    gene = db[gene_id]
    print(gene)
    print(gene.seqid, gene.strand)
    

gene_id 是从 VectorBase 获取的,它是一个专门用于疾病传播媒介基因组学的在线数据库。对于其他特定情况,你需要知道你的基因 ID(它将依赖于物种和数据库)。输出将如下所示:

AgamP4_2L       VEuPathDB       protein_coding_gene     2358158 2431617 .       +       .       ID=AGAP004707;Name=para;description=voltage-gated sodium channel
AgamP4_2L + 

注意该基因位于 2L 染色体臂,并且是以正向方向编码的(+ 链)。

  1. 让我们将 2L 染色体臂的序列保存在内存中(它只有一个染色体,所以我们可以稍微放松一些):

    recs = SeqIO.parse(gzip.open('gambiae.fa.gz', 'rt', encoding='utf-8'), 'fasta')
    for rec in recs:
        print(rec.description)
        if rec.id == gene.seqid:
            my_seq = rec.seq
            break
    

输出将如下所示:

AgamP4_2L | organism=Anopheles_gambiae_PEST | version=AgamP4 | length=49364325 | SO=chromosome
  1. 让我们创建一个函数来为一系列 CDS 构建基因序列:

    def get_sequence(chrom_seq, CDSs, strand):
        seq = Seq.Seq('')
        for CDS in CDSs:
            my_cds = Seq.Seq(str(my_seq[CDS.start - 1:CDS.end]))
            seq += my_cds
        return seq if strand == '+' else seq.reverse_complement()
    

这个函数将接收一个染色体序列(在我们的案例中是 2L 臂),一个编码序列的列表(从注释文件中提取),以及链的信息。

我们必须非常小心序列的起始和结束(注意 GFF 文件是基于 1 的,而 Python 数组是基于 0 的)。最后,如果链是负向的,我们将返回反向互补序列。

  1. 尽管我们已经得到了 gene_id,但我们只需要选择这个基因的三个转录本中的一个,因此需要选择一个:

    mRNAs = db.children(gene, featuretype='mRNA')
    for mRNA in mRNAs:
        print(mRNA.id)
        if mRNA.id.endswith('RA'):
            break
    
  2. 现在,让我们获取转录本的编码序列,然后获取基因序列,并进行翻译:

    CDSs = db.children(mRNA, featuretype='CDS', order_by='start')
    gene_seq = get_sequence(my_seq, CDSs, gene.strand)
    print(len(gene_seq), gene_seq)
    prot = gene_seq.translate()
    print(len(prot), prot)
    
  3. 让我们获取以负链方向编码的基因。我们将提取 VGSC 旁边的基因(恰好是负链)。

    reverse_transcript_id = 'AGAP004708-RA'
    reverse_CDSs = db.children(reverse_transcript_id, featuretype='CDS', order_by='start')
    reverse_seq = get_sequence(my_seq, reverse_CDSs, '-')
    print(len(reverse_seq), reverse_seq)
    reverse_prot = reverse_seq.translate()
    print(len(reverse_prot), reverse_prot)
    

在这里,我避免了获取基因的所有信息,只是硬编码了转录本 ID。关键是你需要确保你的代码无论在什么链上都能正常工作。

还有更多...

这是一个简单的步骤,涵盖了本章和 第三章,《下一代测序》中介绍的几个概念。虽然它在概念上很简单,但不幸的是充满了陷阱。

提示

使用不同的数据库时,确保基因组组装版本是同步的。使用不同版本可能会导致严重且潜在的隐性错误。请记住,不同版本(至少在主版本号上)有不同的坐标。例如,人在基因组 36 版本中 3 号染色体上的位置 1,234 可能与基因组 38 版本中的 1,234 不同,可能指向不同的 SNP。在人类数据中,你可能会发现很多芯片使用的是基因组 36 版本,而整个基因组序列使用的是基因组 37 版本,而最新的人类组装版本是基因组 38 版本。对于我们的Anopheles示例,你将会看到 3 和 4 版本。大多数物种都会遇到这种情况,所以请注意!

还有一个问题是 Python 中的 0 索引数组与 1 索引的基因组数据库之间的差异。不过,需要注意的是,一些基因组数据库可能也使用 0 索引。

这里还有两个容易混淆的点:转录本与基因选择,就像在更丰富的注释数据库中一样。在这里,你将有几个备选的转录本(如果你想查看一个复杂到让人迷惑的数据库,可以参考人类注释数据库)。另外,标记为exon的字段包含的信息比编码序列要多。为了这个目的,你将需要 CDS 字段。

最后,还有一个链条问题,你将需要基于反向互补进行翻译。

另见

以下是一些资源,你可以从中获取更多信息:

使用 Ensembl REST API 查找同源基因

在这个食谱中,我们将学习如何为某个基因寻找同源基因。这个简单的食谱不仅介绍了同源性检索,还教你如何使用网页上的 REST API 来访问生物数据。最后,虽然不是最重要的,它将作为如何使用编程 API 访问 Ensembl 数据库的入门教程。

在我们的示例中,我们将尝试为人类horse基因组寻找任何同源基因。

准备工作

这个食谱不需要任何预先下载的数据,但由于我们使用的是 Web API,因此需要互联网访问。传输的数据量将受到限制。

我们还将使用requests库来访问 Ensembl。请求 API 是一个易于使用的 Web 请求封装。自然,你也可以使用标准的 Python 库,但这些要麻烦得多。

像往常一样,你可以在 Chapter05/Orthology.py 笔记本文件中找到这些内容。

如何操作...

按照以下步骤操作:

  1. 我们将从创建一个支持函数开始,以执行网络请求:

    import requests
    ensembl_server = 'http://rest.ensembl.org'
    def do_request(server, service, *args, **kwargs):
        url_params = ''
        for a in args:
            if a is not None:
                url_params += '/' + a
        req = requests.get('%s/%s%s' % (server, service, url_params), params=kwargs, headers={'Content-Type': 'application/json'})
        if not req.ok:
            req.raise_for_status()
        return req.json()
    

我们首先导入 requests 库并指定根 URL。然后,我们创建一个简单的函数,传入要调用的功能(参见以下示例),并生成完整的 URL。它还会添加可选参数,并指定负载类型为 JSON(这样就能获取默认的 JSON 响应)。它将返回 JSON 格式的响应。通常这是一个嵌套的 Python 数据结构,包含列表和字典。

  1. 接下来,我们将检查服务器上所有可用的物种,写这本书时大约有 110 种物种:

    answer = do_request(ensembl_server, 'info/species')
    for i, sp in enumerate(answer['species']):
        print(i, sp['name'])
    

请注意,这将构造一个以 http://rest.ensembl.org/info/species 为前缀的 URL,用于 REST 请求。顺便说一下,前面的链接在你的浏览器中无法使用,它应该仅通过 REST API 使用。

  1. 现在,让我们尝试查找与人类数据相关的 HGNC 数据库:

    ext_dbs = do_request(ensembl_server, 'info/external_dbs', 'homo_sapiens', filter='HGNC%')
    print(ext_dbs)
    

我们将搜索限制为与人类相关的数据库(homo_sapiens)。我们还会筛选以 HGNC 开头的数据库(这个筛选使用 SQL 表达式)。HGNC 是 HUGO 数据库。我们要确保它可用,因为 HUGO 数据库负责整理人类基因名称并维护我们的 LCT 标识符。

  1. 现在我们知道 LCT 标识符可能可用,我们希望检索该基因的 Ensembl ID,如以下代码所示:

    answer = do_request(ensembl_server, 'lookup/symbol', 'homo_sapiens', 'LCT')
    print(answer)
    lct_id = answer['id']
    

提示

正如你现在可能知道的,不同的数据库会为相同的对象分配不同的 ID。我们需要将我们的 LCT 标识符解析为 Ensembl ID。当你处理与相同对象相关的外部数据库时,数据库之间的 ID 转换可能是你需要完成的第一项任务。

  1. 仅供参考,我们现在可以获取包含基因的区域的序列。请注意,这可能是整个区间,因此如果你想恢复基因,你需要使用类似于我们在前一个步骤中使用的方法:

    lct_seq = do_request(ensembl_server, 'sequence/id', lct_id)
    print(lct_seq)
    
  2. 我们还可以检查 Ensembl 已知的其他数据库;参见以下基因:

    lct_xrefs = do_request(ensembl_server, 'xrefs/id', lct_id)
    for xref in lct_xrefs:
        print(xref['db_display_name'])
        print(xref)
    

你会发现不同种类的数据库,比如 脊椎动物基因组注释 (Vega) 项目、UniProt(参见 第八章使用蛋白质数据库)和 WikiGene。

  1. 让我们获取这个基因在 horse 基因组上的同源基因:

    hom_response = do_request(ensembl_server, 'homology/id', lct_id, type='orthologues', sequence='none')
    homologies = hom_response['data'][0]['homologies']
    for homology in homologies:
        print(homology['target']['species'])
        if homology['target']['species'] != 'equus_caballus':
            continue
        print(homology)
        print(homology['taxonomy_level'])
        horse_id = homology['target']['id']
    

我们本可以通过在 do_request 中指定 target_species 参数,直接获取 horse 基因组的同源基因。然而,这段代码允许你检查所有可用的同源基因。

你将获得关于同源基因的许多信息,例如同源性分类的分类学级别(Boreoeutheria—有胎盘哺乳动物是人类与马匹之间最近的系统发育级别)、同源基因的 Ensembl ID、dN/dS 比率(非同义突变与同义突变的比例),以及 CIGAR 字符串(请参见前一章,第三章下一代测序)来表示序列之间的差异。默认情况下,你还会得到同源序列的比对结果,但为了简化输出,我已经将其移除。

  1. 最后,让我们查找horse_id的 Ensembl 记录:

    horse_req = do_request(ensembl_server, 'lookup/id', horse_id)
    print(horse_req)
    

从这一点开始,你可以使用之前配方中的方法来探索 LCT horse同源基因。

还有更多…

你可以在rest.ensembl.org/找到所有可用功能的详细解释。这包括所有接口和 Python 代码片段等语言。

如果你对同源基因感兴趣,可以通过前面的步骤轻松地从之前的配方中获取这些信息。在调用homology/id时,只需将类型替换为paralogues

如果你听说过 Ensembl,那么你可能也听说过 UCSC 的一个替代服务:基因组浏览器(genome.ucsc.edu/)。从用户界面的角度来看,它们在同一层级。从编程的角度来看,Ensembl 可能更加成熟。访问 NCBI Entrez 数据库在第三章下一代测序中有介绍。

另一种完全不同的编程接口方式是下载原始数据表并将其导入到本地 MySQL 数据库中。请注意,这本身会是一个相当大的工程(你可能只想加载非常小的一部分数据表)。然而,如果你打算进行非常密集的使用,可能需要考虑创建部分数据库的本地版本。如果是这种情况,你可能需要重新考虑 UCSC 的替代方案,因为从本地数据库的角度来看,它和 Ensembl 一样优秀。

从 Ensembl 获取基因本体论信息

在本步骤中,你将再次学习如何通过查询 Ensembl REST API 来使用基因本体论信息。基因本体论是用于注释基因及基因产物的受控词汇。这些词汇以概念树的形式提供(越通用的概念在层次结构的顶部)。基因本体论有三个领域:细胞成分、分子功能和生物过程。

准备工作

与之前的步骤一样,我们不需要任何预下载的数据,但由于我们使用的是 Web API,因此需要互联网访问。传输的数据量将是有限的。

如常,你可以在Chapter05/Gene_Ontology.py笔记本文件中找到这些内容。我们将使用在前一部分(使用 Ensembl REST API 查找直系同源基因)中定义的do_request函数。为了绘制 GO 树,我们将使用pygraphviz,一个图形绘制库:

conda install pygraphviz

好的,我们准备好了。

如何操作...

按照以下步骤操作:

  1. 让我们从检索与 LCT 基因相关的所有 GO 术语开始(你已经在前一部分学会了如何检索 Ensembl ID)。记住,你需要使用前一部分中的do_request函数:

    lct_id = 'ENSG00000115850'
    refs = do_request(ensembl_server, 'xrefs/id', lct_id,external_db='GO', all_levels='1')
    print(len(refs))
    print(refs[0].keys())
    for ref in refs:
        go_id = ref['primary_id']
        details = do_request(ensembl_server, 'ontology/id', go_id)
        print('%s %s %s' % (go_id, details['namespace'], ref['description']))
        print('%s\n' % details['definition'])
    

注意自由格式的定义和每个术语的不同命名空间。在循环中报告的前两个项目如下(当你运行时,它可能会有所变化,因为数据库可能已经更新):

GO:0000016 molecular_function lactase activity
 "Catalysis of the reaction: lactose + H2O = D-glucose + D-galactose." [EC:3.2.1.108]

 GO:0004553 molecular_function hydrolase activity, hydrolyzing O-glycosyl compounds
 "Catalysis of the hydrolysis of any O-glycosyl bond." [GOC:mah]
  1. 让我们集中注意力在乳糖酶活性分子功能上,并获取更多关于它的详细信息(以下go_id来自前一步):

    go_id = 'GO:0000016'
    my_data = do_request(ensembl_server, 'ontology/id', go_id)
    for k, v in my_data.items():
        if k == 'parents':
            for parent in v:
                print(parent)
                parent_id = parent['accession']
        else:
            print('%s: %s' % (k, str(v)))
    parent_data = do_request(ensembl_server, 'ontology/id', parent_id)
    print(parent_id, len(parent_data['children']))
    

我们打印出乳糖酶活性记录(它目前是 GO 树分子功能的一个节点),并检索潜在的父节点列表。此记录只有一个父节点。我们获取该父节点并打印出它的子节点数量。

  1. 让我们检索所有与乳糖酶活性分子功能相关的一般术语(再次强调,父术语及所有其他祖先术语):

    refs = do_request(ensembl_server, 'ontology/ancestors/chart', go_id)
    for go, entry in refs.items():
        print(go)
        term = entry['term']
        print('%s %s' % (term['name'], term['definition']))
        is_a = entry.get('is_a', [])
        print('\t is a: %s\n' % ', '.join([x['accession'] for x in is_a]))
    

我们通过遵循is_a关系来获取祖先列表(更多关于可能关系类型的详细信息请参考另见部分中的 GO 网站)。

  1. 让我们定义一个函数,该函数将创建一个包含术语的祖先关系字典,并返回每个术语的摘要信息,作为一个对:

    def get_upper(go_id):
        parents = {}
        node_data = {}
        refs = do_request(ensembl_server, 'ontology/ancestors/chart', go_id)
        for ref, entry in refs.items():
            my_data = do_request(ensembl_server, 'ontology/id', ref)
            node_data[ref] = {'name': entry['term']['name'], 'children': my_data['children']}
            try:
                parents[ref] = [x['accession'] for x in entry['is_a']]
            except KeyError:
                pass  # Top of hierarchy
        return parents, node_data
    
  2. 最后,我们将打印出乳糖酶活性术语的关系树。为此,我们将使用pygraphivz库:

    parents, node_data = get_upper(go_id)
    import pygraphviz as pgv
    g = pgv.AGraph(directed=True)
    for ofs, ofs_parents in parents.items():
        ofs_text = '%s\n(%s)' % (node_data[ofs]['name'].replace(', ', '\n'), ofs)
        for parent in ofs_parents:
            parent_text = '%s\n(%s)' % (node_data[parent]['name'].replace(', ', '\n'), parent)
            children = node_data[parent]['children']
            if len(children) < 3:
                for child in children:
                    if child['accession'] in node_data:
                        continue
                    g.add_edge(parent_text, child['accession'])
            else:
                g.add_edge(parent_text, '...%d...' % (len(children) - 1))
            g.add_edge(parent_text, ofs_text)
    print(g)
    g.graph_attr['label']='Ontology tree for Lactase activity'
    g.node_attr['shape']='rectangle'
    g.layout(prog='dot')
    g.draw('graph.png')
    

以下输出显示了乳糖酶活性术语的本体树:

图 5.4 – “乳糖酶活性”术语的本体树(顶部的术语更为一般);树的顶部是 molecular_function;对于所有祖先节点,还标注了额外后代的数量(如果少于三个,则进行列举)

图 5.4 – “乳糖酶活性”术语的本体树(顶部的术语更为一般);树的顶部是 molecular_function;对于所有祖先节点,还标注了额外后代的数量(如果少于三个,则进行列举)

还有更多内容...

如果你对基因本体论感兴趣,主要参考网站是geneontology.org,在这里你会找到更多关于这个主题的信息。除了molecular_function,基因本体还包括生物过程细胞组分。在我们的配方中,我们遵循了is a的分层关系,但也存在其他部分关系。例如,“线粒体核糖体”(GO:0005761)是一个细胞组分,是“线粒体基质”的一部分(参考amigo.geneontology.org/amigo/term/GO:0005761#display-lineage-tab,点击图形视图)。

与前面的配方一样,你可以下载基因本体数据库的 MySQL 转储(你可能更喜欢以这种方式与数据交互)。有关详细信息,请参阅geneontology.org/page/download-go-annotations。同样,请准备一些时间来理解关系数据库架构。此外,请注意,绘制树和图形有许多替代方案可用于 Graphviz。我们将在本书后续章节回顾这个主题。

另见

这里有一些可以进一步了解的资源:

第七章:群体遗传学

群体遗传学是研究群体中等位基因频率变化的学科,这些变化基于选择、漂变、突变和迁徙。前几章主要集中在数据处理和清理上;这是我们第一次真正推断有趣的生物学结果。

基于序列数据的群体遗传学分析有很多有趣的内容,但由于我们已经有了不少处理序列数据的操作,我们将把注意力转向其他地方。此外,我们不会在此讨论基因组结构变异,如拷贝数变异CNVs)或倒位。我们将集中分析 SNP 数据,这是最常见的数据类型之一。我们将使用 Python 执行许多标准的群体遗传学分析,例如使用固定指数FST)计算 F 统计量、主成分分析PCA),并研究群体结构。

我们将主要使用 Python 作为脚本语言,来将执行必要计算的应用程序连接在一起,这是一种传统的做法。话虽如此,由于 Python 软件生态系统仍在不断发展,你至少可以使用 scikit-learn 在 Python 中执行 PCA,如我们在第十一章中将看到的那样。

在群体遗传学数据中并没有所谓的默认文件格式。这个领域的严峻现实是,存在大量的文件格式,其中大多数是为特定应用而开发的;因此,没有任何一种是通用的。尽管有一些尝试创建更通用的格式(或者只是开发一个支持多种格式的文件转换器),但这些努力的成功是有限的。更重要的是,随着我们对基因组学理解的不断深入,我们将无论如何需要新的格式(例如,支持某种之前未知的基因组结构变异)。在这里,我们将使用 PLINK(www.cog-genomics.org/plink/2.0/),该工具最初是为在人类数据上执行全基因组关联研究GWAS)而开发的,但它有更多的应用。如果你有下一代测序NGS)数据,可能会问,为什么不使用变异调用格式VCF)?嗯,VCF 文件通常会进行注释,以帮助测序分析,而在这个阶段你并不需要这些(此时你应该已经拥有一个经过过滤的数据集)。如果你将单核苷酸多态性SNP)的调用从 VCF 转换为 PLINK,你会大约节省 95%的存储空间(这是与压缩后的 VCF 相比的结果)。更重要的是,处理 VCF 文件的计算成本要远高于其他两种格式的处理成本(想象一下处理所有这些高度结构化的文本)。如果你使用 Docker,请使用镜像 tiagoantao/bioinformatics_popgen。

本章我们将覆盖以下内容:

  • 使用 PLINK 管理数据集

  • 使用 sgkit 和 xarray 进行群体遗传学分析

  • 使用 sgkit 探索数据集

  • 分析人口结构

  • 执行 PCA 分析

  • 通过混合分析调查人口结构

首先,让我们从文件格式问题的讨论开始,然后继续讨论有趣的数据分析。

使用 PLINK 管理数据集

在这里,我们将使用 PLINK 来管理我们的数据集。我们将从 HapMap 项目中的主数据集中创建适合以下食谱分析的子集。

警告

请注意,PLINK 及其他类似程序并不是为了他们的文件格式而开发的。创建人口遗传学数据的默认文件标准可能并不是一个目标。在这个领域,你需要做好格式转换的准备(为此,Python 非常适合),因为你将使用的每个应用程序可能都有自己的独特需求。从这个食谱中你要学到的最重要的一点是,使用的不是文件格式(尽管这些是相关的),而是一种“文件转换思维”。除此之外,本食谱中的一些步骤还传达了你可能希望使用的真正的分析技巧,例如,子抽样或连锁不平衡LD-)修剪。

准备工作

在本章中,我们将使用国际 HapMap 项目的数据。你可能记得我们在第三章中使用了 1,000 基因组项目的数据,下一代测序,而 HapMap 项目在许多方面是 1,000 基因组项目的前身;它使用的是基因分型,而非全基因组测序。HapMap 项目的大多数样本都用于 1,000 基因组项目,因此如果你已经阅读了第三章中的内容,下一代测序,你就已经对该数据集(包括可用的人群)有所了解。我不会再对数据集做更多介绍,但你可以参考第三章中的内容,下一代测序,以及 HapMap 官网(www.genome.gov/10001688/international-hapmap-project)获取更多信息。请记住,我们有来自世界各地不同人群的基因分型数据。我们将按人群的缩写来引用这些人群。以下是从www.sanger.ac.uk/resources/downloads/human/hapmap3.xhtml获取的列表:

缩写 人群
ASW 美国西南部的非洲血统人群
CEU 来自 CEPH 收藏的北欧和西欧血统的犹他州居民
CHB 中国北京的汉族人
CHD 科罗拉多州丹佛市的华裔居民
GIH 德克萨斯州休斯顿的古吉拉特印度人
JPT 日本东京的日本人
LWK 肯尼亚韦布耶的卢希亚人
MXL 加利福尼亚州洛杉矶的墨西哥裔人
MKK 肯尼亚金亚瓦的马赛人
TSI 意大利托斯卡纳地区的人
YRI 尼日利亚伊巴丹的约鲁巴人

表 6.1 - 基因组计划中的群体

注意

我们将使用 HapMap 项目的数据,该项目实际上已被 1,000 基因组计划所取代。为了教学目的,教授 Python 中的群体遗传学编程技术,HapMap 项目的数据比 1,000 基因组项目更易于处理,因为数据要小得多。HapMap 样本是 1,000 基因组样本的子集。如果你从事人类群体遗传学研究,强烈建议使用 1,000 基因组计划作为基础数据集。

这将需要一个相当大的下载(大约 1GB),并且需要解压。确保你有大约 20GB 的磁盘空间用于本章。文件可以在ftp.ncbi.nlm.nih.gov/hapmap/genotypes/hapmap3_r3/plink_format/找到。

使用以下命令解压 PLINK 文件:

bunzip2 hapmap3_r3_b36_fwd.consensus.qc.poly.map.gz
bunzip2 hapmap3_r3_b36_fwd.consensus.qc.poly.ped.gz

现在,我们有了 PLINK 文件;MAP 文件包含了整个基因组中标记的位置,而 PED 文件包含了每个个体的实际标记,以及一些家谱信息。我们还下载了一个元数据文件,其中包含了每个个体的信息。查看这些文件并熟悉它们。像往常一样,这些内容也可以在Chapter06/Data_Formats.py Notebook 文件中找到,所有内容都已处理好。

最终,这个教程的大部分内容将会大量使用 PLINK(www.cog-genomics.org/plink/2.0/)。Python 主要作为连接语言来调用 PLINK。

如何操作...

请查看以下步骤:

  1. 让我们获取我们样本的元数据。我们将加载每个样本的人口信息,并记录数据集中所有其他个体的后代:

    from collections import defaultdict
    f = open('relationships_w_pops_041510.txt')
    pop_ind = defaultdict(list)
    f.readline() # header
    offspring = []
    for l in f:
        toks = l.rstrip().split('\t')
        fam_id = toks[0]
        ind_id = toks[1]
        mom = toks[2]
        dad = toks[3]
        if mom != '0' or dad != '0':
            offspring.append((fam_id, ind_id))
        pop = toks[-1]
    pop_ind[pop].append((fam_id, ind_id))
    f.close()
    

这将加载一个字典,其中人口是键(CEUYRI等),而其值是该人口中个体的列表。该字典还将存储个体是否为其他人的后代信息。每个个体通过家族和个体 ID 进行标识(这些信息可以在 PLINK 文件中找到)。HapMap 项目提供的文件是一个简单的制表符分隔文件,不难处理。虽然我们使用标准的 Python 文本处理读取文件,但这是一个典型的例子,pandas 会有所帮助。

这里有一个重要的点:之所以将此信息提供在一个单独的临时文件中,是因为 PLINK 格式没有考虑到种群结构(该格式仅为 PLINK 设计时所用的病例与对照信息提供了支持)。这并不是格式的缺陷,因为它从未被设计用来支持标准的种群遗传学研究(它是一个 GWAS 工具)。然而,这是种群遗传学中数据格式的普遍特征:无论你最终使用什么格式,都会有所缺失。

我们将在本章的其他例子中使用这些元数据。我们还将进行元数据与 PLINK 文件之间的一致性分析,但我们将把这部分推迟到下一个例子。

  1. 现在,让我们以 10% 和 1% 的标记数对数据集进行子抽样,具体如下:

    import os
    os.system('plink2 --pedmap hapmap3_r3_b36_fwd.consensus.qc.poly --out hapmap10 --thin 0.1 --geno 0.1 --export ped')
    os.system('plink2 --pedmap hapmap3_r3_b36_fwd.consensus.qc.poly --out hapmap1 --thin 0.01 --geno 0.1 --export ped')
    

在 Jupyter Notebook 中,你只需要这样做:

!plink2 --pedmap hapmap3_r3_b36_fwd.consensus.qc.poly --out hapmap10 --thin 0.1 --geno 0.1 --export ped
!plink2 --pedmap hapmap3_r3_b36_fwd.consensus.qc.poly --out hapmap1 --thin 0.01 --geno 0.1 --export ped

注意微妙之处,你实际上并不会得到 1% 或 10% 的数据;每个标记有 1% 或 10% 的机会被选中,因此你将得到大约 1% 或 10% 的标记。

显然,由于过程是随机的,不同的运行会产生不同的标记子集。这将在后续的分析中产生重要的影响。如果你想复现完全相同的结果,你仍然可以使用 --seed 选项。

我们还将去除所有基因型率低于 90% 的 SNP(使用 --geno 0.1 参数)。

注意

这段代码与 Python 本身没有什么特殊之处,但你可能有两个原因希望对数据进行子抽样。首先,如果你正在对自己的数据集进行探索性分析,你可能希望从一个较小的版本开始,因为这样更易处理。而且,你将能更全面地查看你的数据。第二,一些分析方法可能不需要全部数据(实际上,一些方法甚至可能无法使用全部数据)。不过,对于最后一点要非常小心;也就是说,对于每种分析方法,确保你理解要回答的科学问题对数据的要求。通常提供过多的数据是可以的(即便你需要支付时间和内存的代价),但提供过少的数据将导致不可靠的结果。

  1. 现在,让我们只使用常染色体生成子集(也就是说,我们将去除性染色体和线粒体),具体如下:

    def get_non_auto_SNPs(map_file, exclude_file):
        f = open(map_file)
        w = open(exclude_file, 'w')
        for l in f:
            toks = l.rstrip().split('\t')
            try:
                chrom = int(toks[0])
            except ValueError:
                rs = toks[1]
                w.write('%s\n' % rs)
        w.close()
    get_non_auto_SNPs('hapmap1.map', 'exclude1.txt')
    get_non_auto_SNPs('hapmap10.map', 'exclude10.txt')
    os.system('plink2 –-pedmap hapmap1 --out hapmap1_auto --exclude exclude1.txt --export ped')
    os.system('plink2 --pedmap hapmap10 --out hapmap10_auto --exclude exclude10.txt --export ped')
    
  2. 让我们创建一个函数,生成一个包含所有不属于常染色体的 SNP 列表。对于人类数据,这意味着所有非数字染色体。如果你使用的是其他物种,要小心染色体编码,因为 PLINK 是面向人类数据的。如果你的物种是二倍体,具有少于 23 条常染色体,并且有性别决定系统,即 X/Y,这将很简单;如果不是,请参考 www.cog-genomics.org/plink2/input#allow_extra_chr 以获取一些替代方案(例如 --allow-extra-chr 标志)。

  3. 然后,我们为 10%和 1%的子样本数据集创建仅包含常染色体的 PLINK 文件(分别以hapmap10_autohapmap1_auto为前缀)。

  4. 让我们创建一些没有后代的数据集。这些数据集将用于大多数种群遗传学分析,这些分析要求个体之间在一定程度上没有亲缘关系:

    os.system('plink2 --pedmap hapmap10_auto --filter-founders --out hapmap10_auto_noofs --export ped')
    

注意

这一步代表了大多数种群遗传学分析的事实,即这些分析要求样本之间有一定程度的非亲缘关系。显然,由于我们知道一些后代存在于 HapMap 中,因此我们需要将其去除。

然而,请注意,使用你的数据集时,你需要比这更加精细。例如,运行plink --genome或者使用其他程序来检测相关个体。这里的关键是,你必须花费一些精力去检测样本中的相关个体;这并不是一项微不足道的任务。

  1. 我们还将生成一个 LD 修剪后的数据集,这是许多 PCA 和混合分析算法所要求的,具体如下:

    os.system('plink2 --pedmap hapmap10_auto_noofs --indep-pairwise 50 10 0.1 --out keep --export ped')
    os.system('plink2 --pedmap hapmap10_auto_noofs --extract keep.prune.in --recode --out hapmap10_auto_noofs_ld --export ped')
    

第一步生成一个标记列表,供数据集进行 LD 修剪时使用。这使用一个50个 SNP 的滑动窗口,每次推进10个 SNP,切割值为0.1。第二步从之前生成的列表中提取 SNP。

  1. 让我们将几个案例以不同格式重新编码:

    os.system('plink2 --file hapmap10_auto_noofs_ld --recode12 tab --out hapmap10_auto_noofs_ld_12 --export ped 12')
    os.system('plink2 --make-bed --file hapmap10_auto_noofs_ld --out hapmap10_auto_noofs_ld')
    

第一个操作将把一个使用 ACTG 核苷酸字母的 PLINK 格式转换为另一种格式,这种格式将等位基因重新编码为12。我们将在执行 PCA的配方中稍后使用这个。

第二个操作将文件重新编码为二进制格式。如果你在 PLINK 中工作(使用 PLINK 提供的许多有用操作),二进制格式可能是最合适的格式(例如,提供更小的文件大小)。我们将在混合分析配方中使用此格式。

  1. 我们还将提取单一的染色体(2)进行分析。我们将从 10%子样本的常染色体数据集开始:

    os.system('plink2 --pedmap hapmap10_auto_noofs --chr 2 --out hapmap10_auto_noofs_2 --export ped')
    

还有更多……

你可能有许多理由想要创建不同的数据集进行分析。你可能想对数据进行一些快速的初步探索——例如,如果你计划使用的分析算法对数据格式有要求,或对输入有某些约束,例如标记数量或个体之间的关系。很可能你会有许多子集需要分析(除非你的数据集本身就非常小,例如微卫星数据集)。

这可能看起来是一个小细节,但实际上并非如此:在命名文件时要非常小心(请注意,我在生成文件名时遵循了一些简单的约定)。确保文件名能提供有关子集选项的一些信息。在进行后续分析时,你需要确保选择正确的数据集;你希望你的数据集管理是灵活且可靠的,最重要的是。最糟糕的情况是,你创建了一个包含错误数据集的分析,而这个数据集不符合软件要求的约束条件。

我们使用的 LD 剪枝对于人类分析来说是标准的,但如果你使用非人类数据,请务必检查参数。

我们下载的 HapMap 文件是基于旧版参考基因组(构建 36)。如前一章 第五章 所述,与基因组合作,如果你计划使用此文件进行更多分析,请确保使用构建 36 的注释。

本示例为接下来的示例做了准备,其结果将被广泛使用。

另请参见

使用 sgkit 进行群体遗传学分析与 xarray

Sgkit 是进行群体遗传学分析的最先进的 Python 库。它是一个现代实现,利用了几乎所有 Python 中的基础数据科学库。当我说几乎所有时,我并没有夸张;它使用了 NumPy、pandas、xarray、Zarr 和 Dask。NumPy 和 pandas 在 第二章 中已介绍。在这里,我们将介绍 xarray 作为 sgkit 的主要数据容器。因为我觉得不能要求你对数据工程库有极为深入的了解,所以我会略过 Dask 部分(主要是将 Dask 结构当作等价的 NumPy 结构来处理)。你可以在 第十一章 中找到有关超大内存 Dask 数据结构的更详细信息。

准备工作

你需要运行之前的示例,因为它的输出是本示例所需的:我们将使用其中一个 PLINK 数据集。你需要安装 sgkit。

与往常一样,这可以在 Chapter06/Sgkit.py 笔记本文件中找到,但你仍然需要运行之前的笔记本文件以生成所需的文件。

如何操作...

看一下以下步骤:

  1. 让我们加载在之前的示例中生成的 hapmap10_auto_noofs_ld 数据集:

    import numpy as np
    from sgkit.io import plink
    data = plink.read_plink(path='hapmap10_auto_noofs_ld', fam_sep='\t')
    

记住我们正在加载一组 PLINK 文件。事实证明,sgkit 为该数据创建了非常丰富且结构化的表示。这种表示基于 xarray 数据集。

  1. 让我们检查一下数据的结构 —— 如果你在笔记本中,只需输入以下内容:

    data
    

sgkit – 如果在笔记本中 – 将生成以下表示:

图 6.1 - sgkit 加载的 xarray 数据概览,适用于我们的 PLINK 文件

图 6.1 - sgkit 加载的 xarray 数据概览,适用于我们的 PLINK 文件

data 是一个 xarray 数据集。xarray 数据集本质上是一个字典,其中每个值都是一个 Dask 数组。就我们而言,可以假设它是一个 NumPy 数组。在这种情况下,我们可以看到我们有 56241 个变异,涵盖了 1198 个样本。每个变异有 2 个等位基因,且倍性为 2

在笔记本中,我们可以展开每一项。在我们的案例中,我们展开了 call_genotype。这是一个三维数组,包含 variantssamplesploidy 维度。该数组的类型是 int8。接下来,我们可以找到一些与条目相关的元数据,如 mixed_ploidy 和注释。最后,你将看到 Dask 实现的摘要。Array 列展示了数组的大小和形状的详细信息。对于 Chunk 列,请参阅 第十一章——但现在可以安全忽略它。

  1. 获取摘要信息的另一种方法,尤其在你没有使用笔记本时,便是检查 dims 字段:

    print(data.dims)
    

输出应该不言自明:

Frozen({'variants': 56241, 'alleles': 2, 'samples': 1198, 'ploidy': 2})
  1. 让我们提取一些关于样本的信息:

    print(len(data.sample_id.values))
    print(data.sample_id.values)
    print(data.sample_family_id.values)
    print(data.sample_sex.values)
    

输出如下:

1198
['NA19916' 'NA19835' 'NA20282' ... 'NA18915' 'NA19250' 'NA19124']
['2431' '2424' '2469' ... 'Y029' 'Y113' 'Y076']
[1 2 2 ... 1 2 1]

我们有 1198 个样本。第一个样本的样本 ID 是 NA19916,家庭 ID 是 2431,性别为 1(男性)。请记住,考虑到 PLINK 作为数据源,样本 ID 并不足以作为主键(你可能会有多个样本具有相同的样本 ID)。主键是样本 ID 和样本家庭 ID 的复合键。

提示

你可能已经注意到我们在所有数据字段后加了 .values:这实际上是将一个懒加载的 Dask 数组渲染成一个具象的 NumPy 数组。现在,我建议你忽略它,但如果你在阅读完 第十一章 后重新回到这一章,.values 类似于 Dask 中的 compute 方法。

.values 调用并不麻烦——我们的代码之所以能工作,是因为我们的数据集足够小,可以适应内存,这对于我们的教学示例来说非常好。但如果你有一个非常大的数据集,前面的代码就过于简单了。再次强调, 第十一章 会帮助你解决这个问题。现在,简单性是为了教学目的。

  1. 在查看变异数据之前,我们必须了解 sgkit 如何存储 contigs

    print(data.contigs)
    

输出如下:

['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19', '20', '21', '22']

这里的 contigs 是人类的常染色体(如果你的数据来自于大多数其他物种,可能就不那么幸运了——你可能会看到一些丑陋的标识符)。

  1. 现在,让我们来看看变异数据:

    print(len(data.variant_contig.values))
    print(data.variant_contig.values)
    print(data.variant_position.values)
    print(data.variant_allele.values)
    print(data.variant_id.values)
    

这是输出的简化版本:

56241
[ 0  0  0 ... 21 21 21]
[  557616   782343   908247 ... 49528105 49531259 49559741]
[[b'G' b'A']
 ...
 [b'C' b'A']]
['rs11510103' 'rs2905036' 'rs13303118' ... 'rs11705587' 'rs7284680'
 'rs2238837']

我们有 56241 个变异。contig 索引是 0,如果你查看前一步的步骤,它代表染色体 1。变异位于位置 557616(参照人类基因组版本 36),可能的等位基因是 GA。它有一个 SNP ID,rs11510103

  1. 最后,让我们看看 genotype 数据:

    call_genotype = data.call_genotype.values
    print(call_genotype.shape)
    first_individual = call_genotype[:,0,:]
    first_variant = call_genotype[0,:,:]
    first_variant_of_first_individual = call_genotype[0,0,:]
    print(first_variant_of_first_individual)
    print(data.sample_family_id.values[0], data.sample_id.values[0])
    print(data.variant_allele.values[0])
    

call_genotype 的形状为 56,241 x 1,1198,2,这是它的变异、样本和倍性维度。

要获取第一个个体的所有变异数据,你需要固定第二个维度。要获取第一个变异的所有样本数据,你需要固定第一个维度。

如果你打印出第一个个体的详细信息(样本和家庭 ID),你会得到 2431NA19916 ——如预期的那样,正如在上一个样本探索中的第一个案例。

还有更多内容...

这个食谱主要是对 xarray 的介绍,伪装成 sgkit 教程。关于 xarray 还有很多内容要说——一定要查看 docs.xarray.dev/。值得重申的是,xarray 依赖于大量的 Python 数据科学库,而我们现在暂时忽略了 Dask。

使用 sgkit 探索数据集

在这个食谱中,我们将对其中一个生成的数据集进行初步的探索性分析。现在我们对 xarray 有了一些基本了解,可以实际尝试进行一些数据分析。在这个食谱中,我们将忽略群体结构问题,这一问题将在下一个食谱中回到。

准备工作

你需要先运行第一个食谱,并确保你有 hapmap10_auto_noofs_ld 文件。此食谱中有一个 Notebook 文件,名为 Chapter06/Exploratory_Analysis.py。你还需要为上一个食谱安装的软件。

如何操作...

看一下以下步骤:

  1. 我们首先使用 sgkit 加载 PLINK 数据,方法与上一个食谱完全相同:

    import numpy as np
    import xarray as xr
    import sgkit as sg
    from sgkit.io import plink
    
    data = plink.read_plink(path='hapmap10_auto_noofs_ld', fam_sep='\t')
    
  2. 让我们请求 sgkit 提供 variant_stats

    variant_stats = sg.variant_stats(data)
    variant_stats
    

输出结果如下:

图 6.2 - sgkit 提供的变异统计数据

图 6.2 - sgkit 提供的变异统计数据

  1. 现在我们来看一下统计数据,variant_call_rate

    variant_stats.variant_call_rate.to_series().describe()
    

这里有更多需要解释的内容,可能看起来不太明显。关键部分是 to_series() 调用。Sgkit 返回给你的是一个 Pandas 序列——记住,sgkit 与 Python 数据科学库高度集成。获得 Series 对象后,你可以调用 Pandas 的 describe 函数并得到以下结果:

count    56241.000000
mean         0.997198
std          0.003922
min          0.964107
25%          0.996661
50%          0.998331
75%          1.000000
max          1.000000
Name: variant_call_rate, dtype: float64

我们的变异呼叫率相当不错,这并不令人惊讶,因为我们在查看的是阵列数据——如果你有一个基于 NGS 的数据集,数据质量可能会更差。

  1. 现在让我们来看一下样本统计数据:

    sample_stats = sg.sample_stats(data)
    sample_stats
    

同样,sgkit 提供了许多现成的样本统计数据:

图 6.3 - 通过调用 sample_stats 获得的样本统计数据

图 6.3 - 通过调用 sample_stats 获得的样本统计数据

  1. 现在我们来看一下样本的呼叫率:

    sample_stats.sample_call_rate.to_series().hist()
    

这次,我们绘制了样本呼叫率的直方图。同样,sgkit 通过利用 Pandas 自动实现这一功能:

图 6.4 - 样本呼叫率的直方图

图 6.4 - 样本呼叫率的直方图

还有更多内容...

事实上,对于人口遗传学分析,R 是无可比拟的;我们强烈建议你查看现有的 R 人口遗传学库。不要忘记,Python 和 R 之间有一个桥接,在 第一章《Python 和周边软件生态》一章中已作讨论。

如果在更大的数据集上执行,大多数在此展示的分析将会消耗大量计算资源。实际上,sgkit 已经准备好应对这个问题,因为它利用了 Dask。虽然在这个阶段介绍 Dask 过于复杂,但对于大数据集,第十一章 会讨论如何解决这些问题。

另见

  • 统计遗传学的 R 包列表可以在 cran.r-project.org/web/views/Genetics.xhtml 找到。

  • 如果你需要了解更多人口遗传学的内容,我推荐阅读 Daniel L. Hartl 和 Andrew G. Clark 合著的《人口遗传学原理》一书,出版商是 Sinauer Associates

分析人群结构

之前,我们介绍了不考虑人群结构的 sgkit 数据分析。事实上,大多数数据集,包括我们正在使用的这个数据集,都有一定的人群结构。sgkit 提供了分析具有群体结构的基因组数据集的功能,我们将在这里进行探讨。

准备工作

你需要先运行第一个配方,并且应该已经下载了我们生成的 hapmap10_auto_noofs_ld 数据和原始的人群元数据 relationships_w_pops_041510.txt 文件。配方文件中有一个 Notebook 文件,包含了 06_PopGen/Pop_Stats.py

如何操作...

请看以下步骤:

  1. 首先,让我们用 sgkit 加载 PLINK 数据:

    from collections import defaultdict
    from pprint import pprint
    import numpy as np
    import matplotlib.pyplot as plt
    import seaborn as sns
    import pandas as pd
    import xarray as xr
    import sgkit as sg
    from sgkit.io import plink
    
    data = plink.read_plink(path='hapmap10_auto_noofs_ld', fam_sep='\t')
    
  2. 现在,让我们加载数据并将个体分配到各个群体:

    f = open('relationships_w_pops_041510.txt')
    pop_ind = defaultdict(list)
    f.readline()  # header
    for line in f:
        toks = line.rstrip().split('\t')
        fam_id = toks[0]
        ind_id = toks[1]
        pop = toks[-1]
        pop_ind[pop].append((fam_id, ind_id))
    pops = list(pop_ind.keys())
    

我们最终得到一个字典 pop_ind,其中键是人群代码,值是样本列表。请记住,样本的主键是家庭 ID 和样本 ID。

我们还在 pops 变量中列出了人群列表。

  1. 我们现在需要告知 sgkit 每个样本属于哪个人群或队列:

    def assign_cohort(pops, pop_ind, sample_family_id, sample_id):
        cohort = []
        for fid, sid in zip(sample_family_id, sample_id):
            processed = False
            for i, pop in enumerate(pops):
                if (fid, sid) in pop_ind[pop]:
                    processed = True
                    cohort.append(i)
                    break
            if not processed:
                raise Exception(f'Not processed {fid}, {sid}')
        return cohort
    cohort = assign_cohort(pops, pop_ind, data.sample_family_id.values, data.sample_id.values)
    data['sample_cohort'] = xr.DataArray(
        cohort, dims='samples')
    

请记住,sgkit 中的每个样本都有一个在数组中的位置。因此,我们需要创建一个数组,其中每个元素都指向样本内的特定人群或队列。assign_cohort 函数正是做了这件事:它获取我们从 relationships 文件加载的元数据和来自 sgkit 文件的样本列表,并为每个样本获取人群索引。

  1. 现在我们已经将人群信息结构加载到 sgkit 数据集中,接下来可以开始在人口或队列层级计算统计数据。首先,让我们计算每个人群的单一等位基因位点数量:

    cohort_allele_frequency = sg.cohort_allele_frequencies(data)['cohort_allele_frequency'].values
    monom = {}
    for i, pop in enumerate(pops):
        monom[pop] = len(list(filter(lambda x: x, np.isin(cohort_allele_frequency[:, i, 0], [0, 1]))))
    pprint(monom)
    

我们首先请求 sgkit 计算每个队列或人群的等位基因频率。然后,我们筛选每个人群中的所有位点,其中第一个等位基因的频率为01(即其中一个等位基因已经固定)。最后,我们打印出来。顺便提一下,我们使用pprint.pprint函数让输出看起来更清晰(如果你希望以可读的方式渲染输出,且结构较复杂时,这个函数非常有用):

{'ASW': 3332,
 'CEU': 8910,
 'CHB': 11130,
 'CHD': 12321,
 'GIH': 8960,
 'JPT': 13043,
 'LWK': 3979,
 'MEX': 6502,
 'MKK': 3490,
 'TSI': 8601,
 'YRI': 5172}
  1. 让我们获取每个人群所有位点的最小等位基因频率。这仍然是基于cohort_allele_frequency——因此不需要再次调用 sgkit:

    mafs = {}
    for i, pop in enumerate(pops):
        min_freqs = map(
            lambda x: x if x < 0.5 else 1 - x,
            filter(
                lambda x: x not in [0, 1],
                cohort_allele_frequency[:, i, 0]))
        mafs[pop] = pd.Series(min_freqs)
    

我们为每个人群创建 Pandas Series对象,因为这样可以使用许多有用的功能,例如绘图。

  1. 现在,我们将打印YRIJPT人群的 MAF 直方图。我们将利用 Pandas 和 Matplotlib 来完成此操作:

    maf_plot, maf_ax = plt.subplots(nrows=2, sharey=True)
    mafs['YRI'].hist(ax=maf_ax[0], bins=50)
    maf_ax[0].set_title('*YRI*')
    mafs['JPT'].hist(ax=maf_ax[1], bins=50)
    maf_ax[1].set_title('*JPT*')
    maf_ax[1].set_xlabel('MAF')
    

我们让 Pandas 生成直方图,并将结果放入 Matplotlib 图中。结果如下所示:

图 6.5 - YRI 和 JPT 人群的 MAF 直方图

图 6.5 - YRI 和 JPT 人群的 MAF 直方图

  1. 现在我们将集中计算 FST。FST 是一个广泛使用的统计量,旨在表示由人群结构产生的遗传变异。让我们使用 sgkit 来计算它:

    fst = sg.Fst(data)
    fst = fst.assign_coords({"cohorts_0": pops, "cohorts_1": pops})
    

第一行计算fst,在这种情况下,它将是队列或人群之间的配对fst。第二行通过使用 xarray 坐标功能为每个队列分配名称。这使得代码更简洁,更具声明性。

  1. 让我们比较CEUCHB人群与CHBCHD人群之间的fst

    remove_nan = lambda data: filter(lambda x: not np.isnan(x), data)
    ceu_chb = pd.Series(remove_nan(fst.stat_Fst.sel(cohorts_0='CEU', cohorts_1='CHB').values))
    chb_chd = pd.Series(remove_nan(fst.stat_Fst.sel(cohorts_0='CHB', cohorts_1='CHD').values))
    ceu_chb.describe()
    chb_chd.describe()
    

我们将stat_FSTsel函数返回的配对结果用于比较,并创建一个 Pandas Series。请注意,我们可以按名称引用人群,因为我们已经在前一步中准备好了坐标。

  1. 让我们基于多位点配对 FST 绘制人群间的距离矩阵。在此之前,我们将准备计算:

    mean_fst = {}
    for i, pop_i in enumerate(pops):
        for j, pop_j in enumerate(pops):
            if j <= i:
                continue
            pair_fst = pd.Series(remove_nan(fst.stat_Fst.sel(cohorts_0=pop_i, cohorts_1=pop_j).values))
            mean = pair_fst.mean()
            mean_fst[(pop_i, pop_j)] = mean
    min_pair = min(mean_fst.values())
    max_pair = max(mean_fst.values())
    

我们计算所有人群对之间的 FST 值。执行这段代码时将消耗大量时间和内存,因为我们实际上要求 Dask 进行大量计算,以呈现我们的 NumPy 数组。

  1. 现在我们可以对所有人群的平均 FST 进行配对绘图:

    sns.set_style("white")
    num_pops = len(pops)
    arr = np.ones((num_pops - 1, num_pops - 1, 3), dtype=float)
    fig = plt.figure(figsize=(16, 9))
    ax = fig.add_subplot(111)
    for row in range(num_pops - 1):
        pop_i = pops[row]
        for col in range(row + 1, num_pops):
            pop_j = pops[col]
            val = mean_fst[(pop_i, pop_j)]
            norm_val = (val - min_pair) / (max_pair - min_pair)
            ax.text(col - 1, row, '%.3f' % val, ha='center')
            if norm_val == 0.0:
                arr[row, col - 1, 0] = 1
                arr[row, col - 1, 1] = 1
                arr[row, col - 1, 2] = 0
            elif norm_val == 1.0:
                arr[row, col - 1, 0] = 1
                arr[row, col - 1, 1] = 0
                arr[row, col - 1, 2] = 1
            else:
                arr[row, col - 1, 0] = 1 - norm_val
                arr[row, col - 1, 1] = 1
                arr[row, col - 1, 2] = 1
    ax.imshow(arr, interpolation='none')
    ax.set_title('Multilocus Pairwise FST')
    ax.set_xticks(range(num_pops - 1))
    ax.set_xticklabels(pops[1:])
    ax.set_yticks(range(num_pops - 1))
    ax.set_yticklabels(pops[:-1])
    

在下面的图表中,我们将绘制一个上三角矩阵,其中单元格的背景颜色表示分化的度量;白色表示差异较小(较低的 FST),蓝色表示差异较大(较高的 FST)。CHBCHD之间的最小值用黄色表示,而JPTYRI之间的最大值用洋红色表示。每个单元格中的值是这两个人群之间的平均配对 FST:

图 6.6 - HapMap 项目中 11 个人群所有常染色体的平均配对 FST

图 6.6 - HapMap 项目中 11 个人群所有常染色体的平均配对 FST

另见

  • F 统计量是一个非常复杂的话题,所以我首先将引导你到维基百科页面:en.wikipedia.org/wiki/F-statistics

  • Holsinger 和 Weir 的论文中提供了一个很好的解释,论文标题为《Genetics in geographically structured populations: defining, estimating, and interpreting FST》,刊登在《Nature Reviews Genetics》杂志上,链接:www.nature.com/nrg/journal/v10/n9/abs/nrg2611.xhtml

执行 PCA

PCA 是一种统计方法,用于将多个变量的维度降低到一个较小的子集,这些子集是线性无关的。它在群体遗传学中的实际应用是帮助可视化被研究个体之间的关系。

本章中的大多数食谱都使用 Python 作为粘合语言(Python 调用外部应用程序来完成大部分工作),而在 PCA 中,我们有一个选择:我们可以使用外部应用程序(例如,EIGENSOFT SmartPCA),也可以使用 scikit-learn 并在 Python 中执行所有操作。在本食谱中,我们将使用 SmartPCA——如果你想体验使用 scikit-learn 的原生机器学习方法,可以参考第十章

提示

事实上,你还有第三个选择:使用 sgkit。然而,我想向你展示如何执行计算的替代方案。这样做有两个很好的理由。首先,你可能不想使用 sgkit——尽管我推荐它,但我不想强迫你;其次,你可能需要使用一个在 sgkit 中没有实现的替代方法。PCA 实际上就是一个很好的例子:论文的审稿人可能要求你运行一个已发布且广泛使用的方法,例如 EIGENSOFT SmartPCA。

准备工作

你需要先运行第一个食谱,才能使用hapmap10_auto_noofs_ld_12 PLINK 文件(其中等位基因已重新编码为12)。PCA 需要 LD 修剪后的标记;我们不会在这里使用后代数据,因为这可能会偏向结果。我们将使用重新编码后的 PLINK 文件,其中等位基因为12,因为这样可以使 SmartPCA 和 scikit-learn 的处理更加方便。

我有一个简单的库来帮助进行一些基因组处理。你可以在github.com/tiagoantao/pygenomics找到这个代码。你可以使用以下命令进行安装:

pip install pygenomics

对于本食谱,你需要下载 EIGENSOFT(www.hsph.harvard.edu/alkes-price/software/),其中包含我们将使用的 SmartPCA 应用程序。

Chapter06/PCA.py食谱中有一个 Notebook 文件,但你仍然需要先运行第一个食谱。

如何做...

请查看以下步骤:

  1. 让我们加载元数据,步骤如下:

    f = open('relationships_w_pops_041510.txt')
    ind_pop = {}
    f.readline() # header
    for l in f:
        toks = l.rstrip().split('\t')
        fam_id = toks[0]
        ind_id = toks[1]
        pop = toks[-1]
        ind_pop['/'.join([fam_id, ind_id])] = pop
    f.close()
    ind_pop['2469/NA20281'] = ind_pop['2805/NA20281']
    

在这种情况下,我们将添加一个与 PLINK 文件中已有内容一致的条目。

  1. 让我们将 PLINK 文件转换为 EIGENSOFT 格式:

    from genomics.popgen.plink.convert import to_eigen
    to_eigen('hapmap10_auto_noofs_ld_12', 'hapmap10_auto_noofs_ld_12')
    

这使用了我编写的一个函数,将 PLINK 数据转换为 EIGENSOFT 格式。这主要是文本处理——并不是最激动人心的代码。

  1. 现在,我们将运行 SmartPCA 并解析其结果,如下所示:

    from genomics.popgen.pca import smart
    ctrl = smart.SmartPCAController('hapmap10_auto_noofs_ld_12')
    ctrl.run()
    wei, wei_perc, ind_comp = smart.parse_evec('hapmap10_auto_noofs_ld_12.evec', 'hapmap10_auto_noofs_ld_12.eval')
    

同样,这将使用 pygenomics 中的几个函数来控制 SmartPCA,然后解析输出。代码是此类操作的典型代码,尽管你可以查看它,但实际上它非常直接。

parse 函数将返回 PCA 权重(我们不会使用这些权重,但你应该检查一下)、归一化权重,以及每个个体的主成分(通常最多到 PC 10)。

  1. 然后,我们绘制 PC 1 和 PC 2,如下所示的代码:

    from genomics.popgen.pca import plot
    plot.render_pca(ind_comp, 1, 2, cluster=ind_pop)
    

这将生成以下图示。我们将提供绘图函数和从元数据中检索的种群信息,这样可以用不同的颜色绘制每个种群。结果与已发布的结果非常相似;我们将找到四个群体。大多数亚洲种群位于顶部,非洲种群位于右侧,欧洲种群位于底部。还有两个混合种群(GIHMEX)位于中间:

图 6.7 - 由 SmartPCA 生成的 HapMap 数据的 PC 1 和 PC 2

图 6.7 - 由 SmartPCA 生成的 HapMap 数据的 PC 1 和 PC 2

注意

请注意,PCA 图可以在任何轴上对称,因为信号并不重要。重要的是簇应该相同,并且个体之间(以及这些簇之间)的距离应该相似。

还有更多...

这里有一个有趣的问题,你应该使用哪种方法——SmartPCA 还是 scikit-learn?我们将在第十章中使用 scikit-learn。结果是相似的,所以如果你正在进行自己的分析,可以自由选择。然而,如果你将结果发表在科学期刊上,SmartPCA 可能是更安全的选择,因为它基于遗传学领域已发布的软件;审稿人可能更倾向于选择这一方法。

另见

使用混合分析研究种群结构

在种群遗传学中,一个典型的分析是由结构程序(web.stanford.edu/group/pritchardlab/structure.xhtml)推广的,这个程序用于研究种群结构。此类软件用于推断存在多少个种群(或有多少个祖先种群生成了当前的种群),并识别潜在的迁徙者和混合个体。结构程序是在很久以前开发的,当时基因型标记数量较少(当时主要是少量微卫星标记),之后开发出了更快的版本,包括同一实验室开发的fastStructurerajanil.github.io/fastStructure/)。在这里,我们将使用 Python 与在 UCLA 开发的同类程序——admixture(dalexander.github.io/admixture/download.xhtml)进行接口。

准备工作

你需要运行第一个步骤才能使用hapmap10_auto_noofs_ld二进制 PLINK 文件。同样,我们将使用经过 LD 修剪且没有后代的 10%自动体样本。

如同前面的步骤,你将使用pygenomics库来协助;你可以在github.com/tiagoantao/pygenomics找到这些代码文件。你可以使用以下命令安装它:

pip install pygenomics

理论上,对于本步骤,你需要下载 admixture(www.genetics.ucla.edu/software/admixture/)。然而,在本案例中,我将提供我们将使用的 HapMap 数据上运行 admixture 的输出,因为运行 admixture 需要很长时间。你可以使用已提供的结果,或者自己运行 admixture。此过程的 Notebook 文件位于Chapter06/Admixture.py中,但你仍然需要先运行该步骤。

如何做到这一点...

看一下以下步骤:

  1. 首先,让我们定义我们感兴趣的k(祖先种群的数量)范围,如下所示:

    k_range = range(2, 10)  # 2..9
    
  2. 让我们对所有的k进行 admixture 分析(或者,你也可以跳过此步骤,使用提供的示例数据):

    for k in k_range:
        os.system('admixture --cv=10 hapmap10_auto_noofs_ld.bed %d > admix.%d' % (k, k))
    

注意

这是进行混合分析的最糟糕方式,如果你按这种方式进行,可能需要超过 3 小时。这是因为它会按顺序运行所有的k值,从29。有两种方法可以加速这一过程:使用混合工具提供的多线程选项(-j),或者并行运行多个应用程序。在这里,我假设最坏的情况,你只有一个核心和线程可用,但你应该能通过并行化更高效地运行。我们将在第十一章中详细讨论这个问题。

  1. 我们将需要 PLINK 文件中个体的顺序,因为混合工具以此顺序输出个体结果:

    f = open('hapmap10_auto_noofs_ld.fam')
    ind_order = []
    for l in f:
        toks = l.rstrip().replace(' ', '\t').split('\t')
        fam_id = toks[0]
        ind_id = toks[1]
        ind_order.append((fam_id, ind_id))
    f.close()
    
  2. 交叉验证误差给出了“最佳”k的衡量标准,如下所示:

    import matplotlib.pyplot as plt
    CVs = []
    for k in k_range:
        f = open('admix.%d' % k)
        for l in f:
            if l.find('CV error') > -1:
                CVs.append(float(l.rstrip().split(' ')[-1]))
                break
        f.close()
    fig = plt.figure(figsize=(16, 9))
    ax = fig.add_subplot(111)
    ax.set_title('Cross-Validation error')
    ax.set_xlabel('K')
    ax.plot(k_range, CVs)
    

以下图表绘制了K值从29之间的交叉验证误差,越低越好。从这个图表中应该可以看出,我们可能需要运行更多的K值(事实上,我们有 11 个种群;如果不是更多的话,至少应该运行到 11),但由于计算成本问题,我们停在了9

关于是否存在“最佳”K,这将是一个非常技术性的讨论。现代科学文献表明,可能没有“最佳”K;这些结果值得一些解释。我认为在解释K结果之前,你应该意识到这一点:

图 6.8 - K 的误差

图 6.8 - K 的误差

  1. 我们需要人口信息的元数据:

    f = open('relationships_w_pops_041510.txt')
    pop_ind = defaultdict(list)
    f.readline() # header
    for l in f:
       toks = l.rstrip().split('\t')
       fam_id = toks[0]
       ind_id = toks[1]
       if (fam_id, ind_id) not in ind_order:
          continue
       mom = toks[2]
       dad = toks[3]
       if mom != '0' or dad != '0':
          continue
     pop = toks[-1]
     pop_ind[pop].append((fam_id, ind_id))
    f.close()
    

我们将忽略 PLINK 文件中没有的个体。

  1. 让我们加载个体组成部分,如下所示:

    def load_Q(fname, ind_order):
        ind_comps = {}
        f = open(fname)
        for i, l in enumerate(f):
            comps = [float(x) for x in l.rstrip().split(' ')]
            ind_comps[ind_order[i]] = comps
        f.close()
        return ind_comps
    comps = {}
    for k in k_range:
        comps[k] = load_Q('hapmap10_auto_noofs_ld.%d.Q' % k, ind_order)
    

混合工具会生成一个文件,包含每个个体的祖先组成部分(例如,查看任何生成的Q文件);将会有与您选择研究的k值相等的组成部分数量。在这里,我们将加载我们研究的所有kQ文件,并将其存储在一个字典中,个体 ID 作为键。

  1. 然后,我们将对个体进行聚类,如下所示:

    from genomics.popgen.admix import cluster
    ordering = {}
    for k in k_range:
        ordering[k] = cluster(comps[k], pop_ind)
    

请记住,个体是通过混合获得祖先种群的组成部分;我们希望根据它们在祖先组成部分上的相似性对它们进行排序(而不是按 PLINK 文件中的顺序)。这并不是一项简单的任务,需要使用聚类算法。

此外,我们并不希望对所有个体进行排序;我们希望按每个种群排序,然后再对每个种群进行排序。

为此,我在github.com/tiagoantao/pygenomics/blob/master/genomics/popgen/admix/__init__.py提供了一些聚类代码。这并不完美,但允许你执行一些看起来合理的绘图。我的代码使用了 SciPy 的聚类代码。我建议你看看(顺便说一句,改进它并不难)。

  1. 在合理的个体顺序下,我们现在可以绘制混合图:

    from genomics.popgen.admix import plot
    plot.single(comps[4], ordering[4])
    fig = plt.figure(figsize=(16, 9))
    plot.stacked(comps, ordering[7], fig)
    

这将产生两个图表;第二个图表显示在下图中(第一个图表实际上是从顶部的第三个混合图的变体)。

第一个图 K = 4 需要每个个体的组分及其顺序。它将按种群排序并分割所有个体。

第二个图将绘制从 K = 29 的一组堆叠混合图。它需要一个 figure 对象(由于堆叠混合物的数量可能很多,图的尺寸会有所不同)。个体的顺序通常会遵循其中一个 K(这里我们选择了 K = 7)。

请注意,所有的 K 都值得一些解释(例如,K = 2 将非洲人口与其他人群分离开来,而 K = 3 则将欧洲人口分离,并展示了 GIHMEX 的混合):

图 6.9 - HapMap 示例的堆叠混合图(K 值介于 2 和 9 之间)

图 6.9 - HapMap 示例的堆叠混合图(K 值介于 2 和 9 之间)

还有更多内容...

不幸的是,你不能仅运行单个混合实例来获得结果。最佳做法实际上是运行 100 个实例,并获得具有最佳对数似然的结果(这在混合输出中报告)。显然,我无法要求你为此配方的每个 7 个不同的 K 运行 100 个实例(我们在谈论两周的计算时间),但如果你想要有可发布的结果,你可能必须执行这些步骤。需要集群(或至少非常好的机器)来运行这个。你可以使用 Python 处理输出并选择最佳对数似然的结果。选择每个 K 的最佳结果后,你可以轻松应用此配方来绘制输出。

第八章:系统发育学

系统发育学是应用分子测序技术来研究生物之间的进化关系。通常用系统发育树来表示这一过程。从基因组数据中计算这些树是一个活跃的研究领域,并具有许多现实世界中的应用。

本书将把之前提到的实际操作方法推向一个新高度:这里的大部分内容都受到了关于埃博拉病毒的最新研究的启发,研究了最近在非洲爆发的埃博拉疫情。该研究名为Genomic surveillance elucidates Ebola virus origin and transmission during the 2014 outbreak,由Gire et al.完成,发表于Science期刊。你可以在pubmed.ncbi.nlm.nih.gov/25214632/阅读到这篇文章。在这里,我们将尝试采用类似的方法,以便得出与该论文相似的结果。

本章我们将使用 DendroPy(一个系统发育学库)和 Biopython。bioinformatics_phylo Docker 镜像包含了所有必需的软件。

本章我们将涵盖以下内容:

  • 为系统发育分析准备数据集

  • 对齐遗传和基因组数据

  • 比较序列

  • 重建系统发育树

  • 递归地玩转树状图

  • 可视化系统发育数据

为系统发育分析准备数据集

在这个步骤中,我们将下载并准备用于分析的数据集。该数据集包含了埃博拉病毒的完整基因组。我们将使用 DendroPy 来下载和准备数据。

准备就绪

我们将从 GenBank 下载完整的基因组,这些基因组来自多个埃博拉疫情爆发,其中包括 2014 年疫情中的多个样本。请注意,导致埃博拉病毒病的病毒种类有很多;2014 年疫情中的主要病毒是 EBOV(即正式命名为扎伊尔埃博拉病毒),这是最常见的一种,但此病还由更多的埃博拉病毒属物种引起;另外四种物种的基因组序列也已被测序。你可以在en.wikipedia.org/wiki/Ebolavirus阅读更多信息。

如果你已经完成了前几章的内容,看到这里可能会对涉及的数据量感到担忧;但这完全不成问题,因为这些是大约 19 kbp 大小的病毒基因组。因此,我们的约 100 个基因组实际上相当小巧。

为了进行这个分析,我们需要安装 dendropy。如果你使用的是 Anaconda,请执行以下操作:

conda install –c bioconda dendropy

像往常一样,这些信息可以在相应的 Jupyter Notebook 文件中找到,文件位于Chapter07/Exploration.py

如何操作...

请查看以下步骤:

  1. 首先,让我们使用 DendroPy 指定我们的数据源,如下所示:

    import dendropy
    from dendropy.interop import genbank
    def get_ebov_2014_sources():
        #EBOV_2014
        #yield 'EBOV_2014', genbank.GenBankDna(id_range=(233036, 233118), prefix='KM')
        yield 'EBOV_2014', genbank.GenBankDna(id_range=(34549, 34563), prefix='KM0')
    def get_other_ebov_sources():
        #EBOV other
        yield 'EBOV_1976', genbank.GenBankDna(ids=['AF272001', 'KC242801'])
        yield 'EBOV_1995', genbank.GenBankDna(ids=['KC242796', 'KC242799'])
        yield 'EBOV_2007', genbank.GenBankDna(id_range=(84, 90), prefix='KC2427')
    def get_other_ebolavirus_sources():
        #BDBV
        yield 'BDBV', genbank.GenBankDna(id_range=(3, 6), prefix='KC54539')
        yield 'BDBV', genbank.GenBankDna(ids=['FJ217161']) #RESTV
        yield 'RESTV', genbank.GenBankDna(ids=['AB050936', 'JX477165', 'JX477166',  'FJ621583', 'FJ621584', 'FJ621585'])
        #SUDV
        yield 'SUDV', genbank.GenBankDna(ids=['KC242783', 'AY729654', 'EU338380', 'JN638998', 'FJ968794', 'KC589025', 'JN638998'])
        #yield 'SUDV', genbank.GenBankDna(id_range=(89, 92), prefix='KC5453')
        #TAFV
        yield 'TAFV', genbank.GenBankDna(ids=['FJ217162'])
    

在这里,我们有三个函数:一个用于检索最新 EBOV 爆发的数据,另一个用于检索以前 EBOV 爆发的数据,第三个用于检索其他物种爆发的数据。

请注意,DendroPy 的 GenBank 接口提供了几种不同的方式来指定要检索的记录列表或范围。一些行已被注释掉,包括下载更多基因组的代码。对于我们的目的,我们将下载的子集已足够。

  1. 现在,我们将创建一组 FASTA 文件;我们将在这里以及未来的食谱中使用这些文件:

    other = open('other.fasta', 'w')
    sampled = open('sample.fasta', 'w')
    for species, recs in get_other_ebolavirus_sources():
        tn = dendropy.TaxonNamespace()
        char_mat = recs.generate_char_matrix(taxon_namespace=tn,
            gb_to_taxon_fn=lambda gb: tn.require_taxon(label='%s_%s' % (species, gb.accession)))
        char_mat.write_to_stream(other, 'fasta')
        char_mat.write_to_stream(sampled, 'fasta')
    other.close()
    ebov_2014 = open('ebov_2014.fasta', 'w')
    ebov = open('ebov.fasta', 'w')
    for species, recs in get_ebov_2014_sources():
        tn = dendropy.TaxonNamespace()
        char_mat = recs.generate_char_matrix(taxon_namespace=tn,
            gb_to_taxon_fn=lambda gb: tn.require_taxon(label='EBOV_2014_%s' % gb.accession))
        char_mat.write_to_stream(ebov_2014, 'fasta')
        char_mat.write_to_stream(sampled, 'fasta')
        char_mat.write_to_stream(ebov, 'fasta')
    ebov_2014.close()
    ebov_2007 = open('ebov_2007.fasta', 'w')
    for species, recs in get_other_ebov_sources():
        tn = dendropy.TaxonNamespace()
        char_mat = recs.generate_char_matrix(taxon_namespace=tn,
            gb_to_taxon_fn=lambda gb: tn.require_taxon(label='%s_%s' % (species, gb.accession)))
        char_mat.write_to_stream(ebov, 'fasta')
        char_mat.write_to_stream(sampled, 'fasta')
        if species == 'EBOV_2007':
            char_mat.write_to_stream(ebov_2007, 'fasta')
    ebov.close()
    ebov_2007.close()
    sampled.close()
    

我们将生成几个不同的 FASTA 文件,包含所有基因组、仅 EBOV 基因组或仅包含 2014 年爆发的 EBOV 样本的文件。在本章中,我们将主要使用包含所有基因组的sample.fasta文件。

请注意使用dendropy函数创建 FASTA 文件,这些文件是通过转换从 GenBank 记录中提取的。FASTA 文件中每个序列的 ID 是通过一个 lambda 函数生成的,该函数使用物种和年份,以及 GenBank 的登录号。

  1. 让我们提取病毒中的四个基因(总共七个),如下所示:

    my_genes = ['NP', 'L', 'VP35', 'VP40']
    def dump_genes(species, recs, g_dls, p_hdls):
        for rec in recs:
            for feature in rec.feature_table:
                if feature.key == 'CDS':
                    gene_name = None
                    for qual in feature.qualifiers:
                        if qual.name == 'gene':
                            if qual.value in my_genes:
                                gene_name = qual.value
                        elif qual.name == 'translation':
                            protein_translation = qual.value
                    if gene_name is not None:
                        locs = feature.location.split('.')
                        start, end = int(locs[0]), int(locs[-1])
                        g_hdls[gene_name].write('>%s_%s\n' % (species, rec.accession))
                        p_hdls[gene_name].write('>%s_%s\n' % (species, rec.accession))
                        g_hdls[gene_name].write('%s\n' % rec.sequence_text[start - 1 : end])
                        p_hdls[gene_name].write('%s\n' % protein_translation)
    g_hdls = {}
    p_hdls = {}
    for gene in my_genes:
        g_hdls[gene] = open('%s.fasta' % gene, 'w')
        p_hdls[gene] = open('%s_P.fasta' % gene, 'w')
    for species, recs in get_other_ebolavirus_sources():
        if species in ['RESTV', 'SUDV']:
            dump_genes(species, recs, g_hdls, p_hdls)
    for gene in my_genes:
        g_hdls[gene].close()
        p_hdls[gene].close()
    

我们首先搜索第一个 GenBank 记录中的所有基因特征(请参阅第三章下一代测序,或国家生物技术信息中心NCBI)文档获取更多细节;虽然我们将在这里使用 DendroPy 而不是 Biopython,但概念是相似的),并将数据写入 FASTA 文件,以提取基因。我们将每个基因放入不同的文件中,只取两个病毒物种。我们还获取了转译的蛋白质,这些蛋白质在每个基因的记录中都有提供。

  1. 让我们创建一个函数,从比对中获取基本统计信息,如下所示:

    def describe_seqs(seqs):
        print('Number of sequences: %d' % len(seqs.taxon_namespace))
        print('First 10 taxon sets: %s' % ' '.join([taxon.label for taxon in seqs.taxon_namespace[:10]]))
        lens = []
        for tax, seq in seqs.items():
            lens.append(len([x for x in seq.symbols_as_list() if x != '-']))
        print('Genome length: min %d, mean %.1f, max %d' % (min(lens), sum(lens) / len(lens), max(lens)))
    

我们的函数采用DnaCharacterMatrix DendroPy 类,并统计分类单元的数量。然后,我们提取每个序列中的所有氨基酸(排除由-表示的缺失)来计算长度,并报告最小、平均和最大大小。有关 API 的更多细节,请查看 DendroPy 文档。

  1. 让我们检查一下 EBOV 基因组的序列并计算基本统计数据,如前所示:

    ebov_seqs = dendropy.DnaCharacterMatrix.get_from_path('ebov.fasta', schema='fasta', data_type='dna')
    print('EBOV')
    describe_seqs(ebov_seqs)
    del ebov_seqs
    

然后我们调用一个函数,得到 25 个序列,最小大小为 18,700,平均大小为 18,925.2,最大大小为 18,959。与真核生物相比,这是一个较小的基因组。

请注意,在最后,内存结构已被删除。这是因为内存占用仍然相当大(DendroPy 是一个纯 Python 库,在速度和内存方面有一些开销)。在加载完整基因组时,要小心内存使用。

  1. 现在,让我们检查另一个埃博拉病毒基因组文件,并统计不同物种的数量:

    print('ebolavirus sequences')
    ebolav_seqs = dendropy.DnaCharacterMatrix.get_from_path('other.fasta', schema='fasta', data_type='dna')
    describe_seqs(ebolav_seqs)
    from collections import defaultdict
    species = defaultdict(int)
    for taxon in ebolav_seqs.taxon_namespace:
        toks = taxon.label.split('_')
        my_species = toks[0]
        if my_species == 'EBOV':
            ident = '%s (%s)' % (my_species, toks[1])
        else:
            ident = my_species
        species[ident] += 1
    for my_species, cnt in species.items():
        print("%20s: %d" % (my_species, cnt))
    del ebolav_seqs
    

每个分类单元的名称前缀表明了物种,我们利用这一点来填充一个计数字典。

接下来详细介绍物种和 EBOV 的分类(图例中 Bundibugyo 病毒=BDBV,Tai Forest 病毒=TAFV,Sudan 病毒=SUDV,Reston 病毒=RESTV;我们有 1 个 TAFV,6 个 SUDV,6 个 RESTV 和 5 个 BDBV)。

  1. 让我们提取病毒中基因的基本统计信息:

    gene_length = {}
    my_genes = ['NP', 'L', 'VP35', 'VP40']
    for name in my_genes:
        gene_name = name.split('.')[0]
        seqs =    
    dendropy.DnaCharacterMatrix.get_from_path('%s.fasta' % name, schema='fasta', data_type='dna')
        gene_length[gene_name] = []
        for tax, seq in seqs.items():
            gene_length[gene_name].append(len([x for x in  seq.symbols_as_list() if x != '-'])
    for gene, lens in gene_length.items():
        print ('%6s: %d' % (gene, sum(lens) / len(lens)))
    

这允许你概览基本的基因信息(即名称和平均大小),如下所示:

NP: 2218
L: 6636
VP35: 990
VP40: 988

还有更多...

这里的大部分工作可能可以通过 Biopython 完成,但 DendroPy 具有更多额外的功能,将在后续的步骤中进行探索。此外,正如你将发现的,它在某些任务(如文件解析)上更为强大。更重要的是,还有另一个 Python 库用于执行系统发育学分析,你应该考虑一下。它叫做 ETE,可以在etetoolkit.org/找到。

另见

对齐基因和基因组数据

在进行任何系统发育分析之前,我们需要对基因和基因组数据进行对齐。在这里,我们将使用 MAFFT(mafft.cbrc.jp/alignment/software/)进行基因组分析。基因分析将使用 MUSCLE(www.drive5.com/muscle/)进行。

准备就绪

要执行基因组对齐,你需要安装 MAFFT。此外,为了进行基因对齐,将使用 MUSCLE。我们还将使用 trimAl(trimal.cgenomics.org/)以自动化方式去除虚假序列和对齐不良的区域。所有包都可以从 Bioconda 获取:

conda install –c bioconda mafft trimal muscle=3.8

如常,这些信息可以在相应的 Jupyter Notebook 文件Chapter07/Alignment.py中找到。你需要先运行之前的 Notebook,因为它会生成这里所需的文件。在本章中,我们将使用 Biopython。

如何操作...

查看以下步骤:

  1. 现在,我们将运行 MAFFT 来对齐基因组,如下面的代码所示。这个任务是 CPU 密集型和内存密集型的,且将花费相当长的时间:

    from Bio.Align.Applications import MafftCommandline
    mafft_cline = MafftCommandline(input='sample.fasta', ep=0.123, reorder=True, maxiterate=1000, localpair=True)
    print(mafft_cline)
    stdout, stderr = mafft_cline()
    with open('align.fasta', 'w') as w:
        w.write(stdout)
    

之前的参数与论文附录中指定的参数相同。我们将使用 Biopython 接口调用 MAFFT。

  1. 让我们使用 trimAl 来修剪序列,如下所示:

    os.system('trimal -automated1 -in align.fasta -out trim.fasta -fasta')
    

在这里,我们只是通过os.system调用应用程序。-automated1参数来自补充材料。

  1. 此外,我们可以运行MUSCLE来对蛋白质进行比对:

    from Bio.Align.Applications import MuscleCommandline
    my_genes = ['NP', 'L', 'VP35', 'VP40']
    for gene in my_genes:
        muscle_cline = MuscleCommandline(input='%s_P.fasta' % gene)
        print(muscle_cline)
        stdout, stderr = muscle_cline()
        with open('%s_P_align.fasta' % gene, 'w') as w:
        w.write(stdout)
    

我们使用 Biopython 来调用外部应用程序。在这里,我们将对一组蛋白质进行比对。

请注意,为了进行分子进化分析,我们必须比较对齐后的基因,而不是蛋白质(例如,比较同义突变和非同义突变)。然而,我们只对齐了蛋白质。因此,我们必须将对齐数据转换为基因序列形式。

  1. 让我们通过找到三个对应于每个氨基酸的核苷酸来对基因进行比对:

    from Bio import SeqIO
    from Bio.Seq import Seq
    from Bio.SeqRecord import SeqRecord
    for gene in my_genes:
        gene_seqs = {}
        unal_gene = SeqIO.parse('%s.fasta' % gene, 'fasta')
        for rec in unal_gene:
            gene_seqs[rec.id] = rec.seq
        al_prot = SeqIO.parse('%s_P_align.fasta' % gene, 'fasta')
        al_genes = []
        for protein in al_prot:
            my_id = protein.id
            seq = ''
            pos = 0
            for c in protein.seq:
                if c == '-':
                    seq += '---'
                else:
                    seq += str(gene_seqs[my_id][pos:pos + 3])
                    pos += 3
            al_genes.append(SeqRecord(Seq(seq), id=my_id))
        SeqIO.write(al_genes, '%s_align.fasta' % gene, 'fasta')
    

该代码获取蛋白质和基因编码。如果在蛋白质中发现空缺,则写入三个空缺;如果发现氨基酸,则写入相应的基因核苷酸。

比较序列

在这里,我们将比较在上一配方中比对的序列。我们将进行基因范围和基因组范围的比较。

准备开始

我们将使用 DendroPy,并且需要前两个配方的结果。像往常一样,这些信息可以在对应的笔记本Chapter07/Comparison.py中找到。

如何操作……

看一下接下来的步骤:

  1. 让我们开始分析基因数据。为简便起见,我们将仅使用来自伊波拉病毒属的另外两种物种的数据,这些数据已包含在扩展数据集中,即雷斯顿病毒(RESTV)和苏丹病毒(SUDV):

    import os
    from collections import OrderedDict
    import dendropy
    from dendropy.calculate import popgenstat
    genes_species = OrderedDict()
    my_species = ['RESTV', 'SUDV']
    my_genes = ['NP', 'L', 'VP35', 'VP40']
    for name in my_genes:
        gene_name = name.split('.')[0]
        char_mat = dendropy.DnaCharacterMatrix.get_from_path('%s_align.fasta' % name, 'fasta')
        genes_species[gene_name] = {}
    
        for species in my_species:
            genes_species[gene_name][species] = dendropy.DnaCharacterMatrix()
        for taxon, char_map in char_mat.items():
            species = taxon.label.split('_')[0]
            if species in my_species:
                genes_species[gene_name][species].taxon_namespace.add_taxon(taxon)
                genes_species[gene_name][species][taxon] = char_map
    

我们得到在第一步中存储的四个基因,并在第二步中对其进行了比对。

我们加载所有文件(格式为 FASTA),并创建一个包含所有基因的字典。每个条目本身也是一个字典,包含 RESTV 或 SUDV 物种,及所有读取的数据。这些数据量不大,只有少量基因。

  1. 让我们打印所有四个基因的一些基本信息,如分离位点数(seg_sites)、核苷酸多样性(nuc_div)、Tajima’s D(taj_d)和 Waterson’s theta(wat_theta)(请查看本配方中更多...部分的链接,了解这些统计数据的相关信息):

    import numpy as np
    import pandas as pd
    summary = np.ndarray(shape=(len(genes_species), 4 * len(my_species)))
    stats = ['seg_sites', 'nuc_div', 'taj_d', 'wat_theta']
    for row, (gene, species_data) in enumerate(genes_species.items()):
        for col_base, species in enumerate(my_species):
            summary[row, col_base * 4] = popgenstat.num_segregating_sites(species_data[species])
            summary[row, col_base * 4 + 1] = popgenstat.nucleotide_diversity(species_data[species])
            summary[row, col_base * 4 + 2] = popgenstat.tajimas_d(species_data[species])
            summary[row, col_base * 4 + 3] = popgenstat.wattersons_theta(species_data[species])
    columns = []
    for species in my_species:
        columns.extend(['%s (%s)' % (stat, species) for stat in stats])
    df = pd.DataFrame(summary, index=genes_species.keys(), columns=columns)
    df # vs print(df)
    
  2. 首先,我们来看一下输出,然后再解释如何构建它:

图 7.1 – 病毒数据集的数据框

图 7.1 – 病毒数据集的数据框

我使用pandas数据框打印结果,因为它非常适合处理这样的操作。我们将用一个 NumPy 多维数组初始化数据框,数组包含四行(基因)和四个统计数据乘以两个物种。

这些统计数据,如分离位点数、核苷酸多样性、Tajima’s D 和 Watterson’s theta,都是通过 DendroPy 计算的。请注意单个数据点在数组中的位置(坐标计算)。

看看最后一行:如果你在 Jupyter 中,只需要将 df 放在末尾,它会渲染出 DataFrame 和单元格输出。如果你不在笔记本中,使用 print(df)(你也可以在笔记本中执行这个操作,但显示效果可能不如直接在 Jupyter 中漂亮)。

  1. 现在,让我们提取类似的信息,但这次是基因组范围的数据,而不仅仅是基因范围。在这种情况下,我们将使用两个埃博拉爆发(2007 年和 2014 年)的子样本。我们将执行一个函数来显示基本统计信息,如下所示:

    def do_basic_popgen(seqs):
        num_seg_sites = popgenstat.num_segregating_sites(seqs)
        avg_pair = popgenstat.average_number_of_pairwise_differences(seqs)
        nuc_div = popgenstat.nucleotide_diversity(seqs)
        print('Segregating sites: %d, Avg pairwise diffs: %.2f, Nucleotide diversity %.6f' % (num_seg_sites, avg_pair, nuc_div))
        print("Watterson's theta: %s" % popgenstat.wattersons_theta(seqs))
        print("Tajima's D: %s" % popgenstat.tajimas_d(seqs))
    

到现在为止,鉴于之前的例子,这个函数应该很容易理解。

  1. 现在,让我们正确地提取数据的子样本,并输出统计信息:

    ebov_seqs = dendropy.DnaCharacterMatrix.get_from_path(
        'trim.fasta', schema='fasta', data_type='dna')
    sl_2014 = []
    drc_2007 = []
    ebov2007_set = dendropy.DnaCharacterMatrix()
    ebov2014_set = dendropy.DnaCharacterMatrix()
    for taxon, char_map in ebov_seqs.items():
        print(taxon.label)
        if taxon.label.startswith('EBOV_2014') and len(sl_2014) < 8:
            sl_2014.append(char_map)
            ebov2014_set.taxon_namespace.add_taxon(taxon)
            ebov2014_set[taxon] = char_map
        elif taxon.label.startswith('EBOV_2007'):
            drc_2007.append(char_map)
            ebov2007_set.taxon_namespace.add_taxon(taxon)
            ebov2007_set[taxon] = char_map
            #ebov2007_set.extend_map({taxon: char_map})
    del ebov_seqs
    print('2007 outbreak:')
    print('Number of individuals: %s' % len(ebov2007_set.taxon_set))
    do_basic_popgen(ebov2007_set)
    print('\n2014 outbreak:')
    print('Number of individuals: %s' % len(ebov2014_set.taxon_set))
    do_basic_popgen(ebov2014_set)
    

在这里,我们将构建两个数据集的两个版本:2014 年爆发和 2007 年爆发。我们将生成一个版本作为 DnaCharacterMatrix,另一个作为列表。我们将在这个食谱的最后使用这个列表版本。

由于 2014 年埃博拉爆发的数据集很大,我们仅用 8 个个体进行子样本抽取,这与 2007 年爆发的数据集的样本大小相当。

再次,我们删除 ebov_seqs 数据结构以节省内存(这些是基因组,而不仅仅是基因)。

如果你对 GenBank 上可用的 2014 年爆发的完整数据集(99 个样本)执行此分析,请准备好等待相当长的时间。

输出如下所示:

2007 outbreak:
Number of individuals: 7
Segregating sites: 25, Avg pairwise diffs: 7.71, Nucleotide diversity 0.000412
Watterson's theta: 10.204081632653063
Tajima's D: -1.383114157484101
2014 outbreak:
Number of individuals: 8
Segregating sites: 6, Avg pairwise diffs: 2.79, Nucleotide diversity 0.000149
Watterson's theta: 2.31404958677686
Tajima's D: 0.9501208027581887
  1. 最后,我们对 2007 年和 2014 年的两个子集进行一些统计分析,如下所示:

    pair_stats = popgenstat.PopulationPairSummaryStatistics(sl_2014, drc_2007)
    print('Average number of pairwise differences irrespective of population: %.2f' % pair_stats.average_number_of_pairwise_differences)
    print('Average number of pairwise differences between populations: %.2f' % pair_stats.average_number_of_pairwise_differences_between)
    print('Average number of pairwise differences within populations: %.2f' % pair_stats.average_number_of_pairwise_differences_within)
    print('Average number of net pairwise differences : %.2f' % pair_stats.average_number_of_pairwise_differences_net)
    print('Number of segregating sites: %d' % pair_stats.num_segregating_sites)
    print("Watterson's theta: %.2f" % pair_stats.wattersons_theta)
    print("Wakeley's Psi: %.3f" % pair_stats.wakeleys_psi)
    print("Tajima's D: %.2f" % pair_stats.tajimas_d)
    

请注意,我们这里执行的操作稍有不同;我们将要求 DendroPy(popgenstat.PopulationPairSummaryStatistics)直接比较两个种群,以便获得以下结果:

Average number of pairwise differences irrespective of population: 284.46
Average number of pairwise differences between populations: 535.82
Average number of pairwise differences within populations: 10.50
Average number of net pairwise differences : 525.32
Number of segregating sites: 549
Watterson's theta: 168.84
Wakeley's Psi: 0.308
Tajima's D: 3.05

现在,分化位点的数量要大得多,因为我们正在处理来自两个合理分化的不同种群的数据。种群间的平均成对差异数值非常大。如预期的那样,这个数值远大于种群内部的平均数值,无论是否有种群信息。

还有更多……

如果你想获取更多的系统发育学和群体遗传学公式,包括这里使用的那些,我强烈推荐你获取 Arlequin 软件套件的手册(cmpg.unibe.ch/software/arlequin35/)。如果你不使用 Arlequin 进行数据分析,那么它的手册可能是实现公式的最佳参考。这本免费的文档可能包含比任何我能记得的书籍更相关的公式实现细节。

重建系统发育树

在这里,我们将为所有埃博拉物种的对齐数据集构建系统发育树。我们将遵循与论文中使用的过程非常相似的步骤。

准备工作

这个食谱需要 RAxML,这是一个用于最大似然推断大型系统发育树的程序,你可以在 sco.h-its.org/exelixis/software.xhtml 上查看它。Bioconda 也包含它,但它的名称是 raxml。请注意,二进制文件叫做 raxmlHPC。你可以执行以下命令来安装它:

conda install –c bioconda raxml

上面的代码很简单,但执行起来需要时间,因为它会调用 RAxML(这是一个计算密集型过程)。如果你选择使用 DendroPy 接口,它也可能会变得内存密集。我们将与 RAxML、DendroPy 和 Biopython 进行交互,给你选择使用哪个接口的自由;DendroPy 给你提供了一个简单的方式来访问结果,而 Biopython 则是内存占用较少的选择。尽管本章后面有一个可视化的食谱,但我们仍然会在这里绘制我们生成的树。

和往常一样,这些信息可以在相应的笔记本 Chapter07/Reconstruction.py 中找到。你需要前一个食谱的输出才能完成这个食谱。

如何操作…

看一下以下步骤:

  1. 对于 DendroPy,我们将首先加载数据,然后重建属数据集,如下所示:

    import os
    import shutil
    import dendropy
    from dendropy.interop import raxml
    ebola_data = dendropy.DnaCharacterMatrix.get_from_path('trim.fasta', 'fasta')
    rx = raxml.RaxmlRunner()
    ebola_tree = rx.estimate_tree(ebola_data, ['-m', 'GTRGAMMA', '-N', '10'])
    print('RAxML temporary directory %s:' % rx.working_dir_path)
    del ebola_data
    

请记住,这个数据结构的大小相当大;因此,确保你有足够的内存来加载它(至少 10 GB)。

要准备好等待一段时间。根据你的计算机,这可能需要超过一个小时。如果时间过长,考虑重新启动进程,因为有时 RAxML 可能会出现 bug。

我们将使用 GTRΓ 核苷酸替代模型运行 RAxML,如论文中所述。我们只进行 10 次重复以加快结果速度,但你可能应该做更多的重复,比如 100 次。在过程结束时,我们将从内存中删除基因组数据,因为它占用了大量内存。

ebola_data 变量将包含最佳的 RAxML 树,并且包括距离信息。RaxmlRunner 对象将可以访问 RAxML 生成的其他信息。让我们打印出 DendroPy 将执行 RAxML 的目录。如果你检查这个目录,你会发现很多文件。由于 RAxML 返回的是最佳树,你可能会忽略所有这些文件,但我们将在替代的 Biopython 步骤中稍作讨论。

  1. 我们将保存树用于未来的分析;在我们的案例中,它将是一个可视化,如下代码所示:

    ebola_tree.write_to_path('my_ebola.nex', 'nexus')
    

我们将把序列写入 NEXUS 文件,因为我们需要存储拓扑信息。FASTA 在这里不够用。

  1. 让我们可视化我们的属树,如下所示:

    import matplotlib.pyplot as plt
    from Bio import Phylo
    my_ebola_tree = Phylo.read('my_ebola.nex', 'nexus')
    my_ebola_tree.name = 'Our Ebolavirus tree'
    fig = plt.figure(figsize=(16, 18))
    ax = fig.add_subplot(1, 1, 1)
    Phylo.draw(my_ebola_tree, axes=ax)
    

我们将在稍后介绍合适的步骤时再解释这段代码,但如果你查看下图并将其与论文中的结果进行比较,你会清楚地看到它像是朝着正确方向迈进的一步。例如,所有同一物种的个体被聚集在一起。

你会注意到,trimAl 改变了其序列的名称,例如,通过添加它们的大小。这很容易解决;我们将在可视化系统发育数据这一部分中处理这个问题:

图 7.2 – 我们使用 RAxML 生成的所有埃博拉病毒的系统发育树

图 7.2 – 我们使用 RAxML 生成的所有埃博拉病毒的系统发育树

  1. 让我们通过 Biopython 使用 RAxML 重建系统发育树。Biopython 的接口比 DendroPy 更少声明式,但在内存效率上要高得多。因此,在运行完后,你需要负责处理输出,而 DendroPy 会自动返回最优的树,如下代码所示:

    import random
    import shutil
    from Bio.Phylo.Applications import RaxmlCommandline
    raxml_cline = RaxmlCommandline(sequences='trim.fasta', model='GTRGAMMA', name='biopython', num_replicates='10', parsimony_seed=random.randint(0, sys.maxsize), working_dir=os.getcwd() + os.sep + 'bp_rx')
    print(raxml_cline)
    try:
        os.mkdir('bp_rx')
    except OSError:
        shutil.rmtree('bp_rx')
        os.mkdir('bp_rx')
    out, err = raxml_cline()
    

DendroPy 比 Biopython 具有更具声明性的接口,因此你可以处理一些额外的事项。你应该指定种子(如果不指定,Biopython 会自动使用 10,000 作为默认值)以及工作目录。使用 RAxML 时,工作目录的指定要求使用绝对路径。

  1. 让我们检查一下 Biopython 运行的结果。虽然 RAxML 的输出(除了随机性)对于 DendroPy 和 Biopython 是相同的,但 DendroPy 隐藏了几个细节。使用 Biopython,你需要自己处理结果。你也可以使用 DendroPy 来执行这个操作;不过在这种情况下,它是可选的:

    from Bio import Phylo
    biopython_tree = Phylo.read('bp_rx/RAxML_bestTree.biopython', 'newick')
    

上面的代码将读取 RAxML 运行中最优的树。文件名后附加了你在前一步中指定的项目名称(在本例中为biopython)。

看看bp_rx目录的内容;在这里,你将找到来自 RAxML 的所有输出,包括所有 10 个备选树。

还有更多……

尽管本书的目的是不教授系统发育分析,但了解为什么我们不检查树拓扑中的共识和支持信息还是很重要的。你应该在自己的数据集里研究这一点。更多信息,请参考www.geol.umd.edu/~tholtz/G331/lectures/cladistics5.pdf

递归操作树

这不是一本关于 Python 编程的书,因为这个话题非常广泛。话虽如此,入门级 Python 书籍通常不会详细讨论递归编程。通常,递归编程技术非常适合处理树结构。而且,它还是功能性编程方言中的一种必要编程策略,这在进行并发处理时非常有用。这在处理非常大的数据集时是常见的。

系统发育树的概念与计算机科学中的树有所不同。系统发育树可以是有根的(如果是,它们就是普通的树数据结构),也可以是无根的,使其成为无向非循环图。此外,系统发育树的边上可以有权重。因此,在阅读文档时要注意这一点;如果文献是由系统发育学家编写的,你可以期待该树是有根或无根的,而大多数其他文档则会使用无向非循环图来表示无根树。在这个配方中,我们假设所有树都是有根的。

最后,请注意,虽然本配方主要旨在帮助你理解递归算法和树形结构,但最后一部分实际上非常实用,并且对下一个配方的实现至关重要。

准备工作

你需要准备好前一配方中的文件。像往常一样,你可以在 Chapter07/Trees.py 笔记本文件中找到这些内容。在这里,我们将使用 DendroPy 的树表示。请注意,比较其他树表示和库(无论是系统发育的还是非系统发育的)而言,大部分代码都是易于通用的。

如何操作...

看一下以下步骤:

  1. 首先,让我们加载由 RAxML 生成的所有埃博拉病毒树,如下所示:

    import dendropy
    ebola_raxml = dendropy.Tree.get_from_path('my_ebola.nex', 'nexus')
    
  2. 接着,我们需要计算每个节点的层级(到根节点的距离):

    def compute_level(node, level=0):
        for child in node.child_nodes():
            compute_level(child, level + 1)
        if node.taxon is not None:
            print("%s: %d %d" % (node.taxon, node.level(), level))
    compute_level(ebola_raxml.seed_node)
    

DendroPy 的节点表示有一个层级方法(用于比较),但这里的重点是介绍递归算法,所以我们还是会实现它。

注意这个函数的工作原理;它是用 seed_node 调用的(这是根节点,因为代码假设我们处理的是有根树)。根节点的默认层级是 0。然后,该函数会递归调用所有子节点,将层级加一。对于每个非叶子节点(即,它是树的内部节点),这个调用会被重复,直到我们到达叶子节点。

对于叶子节点,我们接着打印出层级(我们也可以对内部节点执行相同的操作),并展示由 DendroPy 内部函数计算出的相同信息。

  1. 现在,让我们计算每个节点的高度。节点的高度是从该节点开始的最大向下路径(通向叶子)的边数,如下所示:

    def compute_height(node):
        children = node.child_nodes()
        if len(children) == 0:
            height = 0
        else:
        height = 1 + max(map(lambda x: compute_height(x), children))
        desc = node.taxon or 'Internal'
        print("%s: %d %d" % (desc, height, node.level()))
        return height
    compute_height(ebola_raxml.seed_node)
    

在这里,我们将使用相同的递归策略,但每个节点将把它的高度返回给其父节点。如果该节点是叶子节点,则高度为 0;如果不是,则高度为 1 加上其所有后代的最大高度。

请注意,我们使用 map 配合 lambda 函数来获取当前节点所有子节点的高度。然后,我们选择最大值(max 函数在这里执行一个 reduce 操作,因为它总结了所有报告的值)。如果你将其与 MapReduce 框架联系起来,你是正确的;它们的灵感来自于像这样的函数式编程方言。

  1. 现在,让我们计算每个节点的子孙数量。到现在为止,这应该很容易理解:

    def compute_nofs(node):
        children = node.child_nodes()
        nofs = len(children)
        map(lambda x: compute_nofs(x), children)
        desc = node.taxon or 'Internal'
        print("%s: %d %d" % (desc, nofs, node.level()))
    compute_nofs(ebola_raxml.seed_node)
    
  2. 现在,我们将打印所有叶节点(显然,这是微不足道的):

    def print_nodes(node):
        for child in node.child_nodes():
            print_nodes(child)
        if node.taxon is not None:
            print('%s (%d)' % (node.taxon, node.level()))
    print_nodes(ebola_raxml.seed_node)
    

请注意,到目前为止,我们开发的所有函数都对树施加了非常明确的遍历模式。它首先调用它的第一个子节点,然后该子节点会调用它的子节点,依此类推;只有这样,函数才能按深度优先的模式调用下一个子节点。然而,我们也可以采取不同的方式。

  1. 现在,让我们以广度优先的方式打印叶节点,也就是说,我们会首先打印离根节点最近的叶节点,按如下方式:

    from collections import deque
    def print_breadth(tree):
        queue = deque()
        queue.append(tree.seed_node)
        while len(queue) > 0:
            process_node = queue.popleft()
            if process_node.taxon is not None:
                print('%s (%d)' % (process_node.taxon, process_node.level()))
            else:
                for child in process_node.child_nodes():
                    queue.append(child)
    print_breadth(ebola_raxml)
    

在我们解释这个算法之前,先看看这个运行的结果与上一次运行的结果有何不同。首先,看看下面的图示。如果你按深度优先顺序打印节点,你会得到 Y、A、X、B 和 C。但是如果你执行广度优先遍历,你会得到 X、B、C、Y 和 A。树的遍历会影响节点的访问顺序;这一点往往非常重要。

关于前面的代码,在这里我们将采用完全不同的方法,因为我们将执行一个迭代算法。我们将使用先进先出FIFO)队列来帮助我们排序节点。请注意,Python 的 deque 可以像 FIFO 一样高效地使用,也可以用于后进先出LIFO)。这是因为它在两端操作时实现了高效的数据结构。

算法从将根节点放入队列开始。当队列不为空时,我们将取出队列中的第一个节点。如果是内部节点,我们将把它的所有子节点放入队列。

我们将继续执行前一步骤,直到队列为空。我鼓励你拿起笔和纸,通过执行下图所示的示例来看看这个过程是如何运作的。代码虽小,但并不简单:

图 7.3 – 遍历树;第一个数字表示在深度优先遍历中访问该节点的顺序,第二个数字则表示广度优先遍历的顺序

图 7.3 – 遍历树;第一个数字表示在深度优先遍历中访问该节点的顺序,第二个数字则表示广度优先遍历的顺序

  1. 让我们回到实际的数据集。由于我们有太多数据无法完全可视化,我们将生成一个精简版,去除包含单一物种的子树(以 EBOV 为例,它们有相同的爆发)。我们还将进行树的阶梯化,即按子节点的数量对子节点进行排序:

    from copy import deepcopy
    simple_ebola = deepcopy(ebola_raxml)
    def simplify_tree(node):
        prefs = set()
        for leaf in node.leaf_nodes():
            my_toks = leaf.taxon.label.split(' ')
            if my_toks[0] == 'EBOV':
                prefs.add('EBOV' + my_toks[1])
            else:
                prefs.add(my_toks[0])
        if len(prefs) == 1:
            print(prefs, len(node.leaf_nodes()))
            node.taxon = dendropy.Taxon(label=list(prefs)[0])
            node.set_child_nodes([])
        else:
            for child in node.child_nodes():
                simplify_tree(child)
    simplify_tree(simple_ebola.seed_node)
    simple_ebola.ladderize()
    simple_ebola.write_to_path('ebola_simple.nex', 'nexus')
    

我们将对树结构进行深拷贝。由于我们的函数和阶梯化过程是破坏性的(它们会改变树结构),我们需要保持原始树结构不变。

DendroPy 能够列举出所有叶节点(在这个阶段,一个好的练习是编写一个函数来执行这个操作)。通过这个功能,我们可以获取某个节点的所有叶子。如果它们与 EBOV 的情况一样,具有相同的物种和爆发年份,我们将移除所有子节点、叶节点和内部子树节点。

如果它们不属于相同物种,我们将递归下去,直到满足条件。最坏的情况是当你已经处于叶节点时,算法会简单地解析为当前节点的物种。

还有更多...

关于树和数据结构的话题,有大量的计算机科学文献;如果你想阅读更多内容,维基百科提供了一个很好的介绍,网址是en.wikipedia.org/wiki/Tree_%28data_structure%29

请注意,lambda函数和map的使用并不被推荐作为 Python 方言;你可以阅读 Guido van Rossum 关于这个主题的一些(较旧的)观点,网址是www.artima.com/weblogs/viewpost.jsp?thread=98196。我在这里呈现它是因为它是函数式和递归编程中的一种非常常见的方言。更常见的方言将基于列表推导式。

无论如何,基于使用mapreduce操作的函数式方言是 MapReduce 框架的概念基础,你可以使用像 Hadoop、Disco 或 Spark 这样的框架来执行高性能的生物信息学计算。

可视化系统发育数据

在这个配方中,我们将讨论如何可视化系统发育树。DendroPy 仅具有基于绘制文本 ASCII 树的简单可视化机制,但 Biopython 拥有非常丰富的基础设施,我们将在这里利用它。

准备工作

这将要求你完成所有前面的配方。记住,我们拥有完整的埃博拉病毒属的文件,包括 RAxML 树。此外,简化版属版本将在前面的配方中生成。如常,你可以在Chapter07/Visualization.py笔记本文件中找到这些内容。

如何实现...

请看下面的步骤:

  1. 让我们加载所有系统发育数据:

    from copy import deepcopy
    from Bio import Phylo
    ebola_tree = Phylo.read('my_ebola.nex', 'nexus')
    ebola_tree.name = 'Ebolavirus tree'
    ebola_simple_tree = Phylo.read('ebola_simple.nex', 'nexus')
    ebola_simple_tree.name = 'Ebolavirus simplified tree'
    

对于我们读取的所有树,我们将更改树的名称,因为稍后会打印出该名称。

  1. 现在,我们可以绘制树的 ASCII 表示:

    Phylo.draw_ascii(ebola_simple_tree)
    Phylo.draw_ascii(ebola_tree)
    

简化版属树的 ASCII 表示如下所示。在这里,我们不会打印完整版本,因为它将占用好几页。但如果你运行前面的代码,你将能够看到它实际上是非常易于阅读的:

图 7.4 – 简化版埃博拉病毒数据集的 ASCII 表示

图 7.4 – 简化版埃博拉病毒数据集的 ASCII 表示

  1. Bio.Phylo通过使用matplotlib作为后端来实现树的图形表示:

    import matplotlib.pyplot as plt
    fig = plt.figure(figsize=(16, 22))
    ax = fig.add_subplot(111)
    Phylo.draw(ebola_simple_tree, branch_labels=lambda c: c.branch_length if c.branch_length > 0.02 else None, axes=ax)
    

在这种情况下,我们将在边缘处打印分支长度,但会去除所有小于 0.02 的长度,以避免杂乱。这样做的结果如下图所示:

图 7.5 – 一个基于 matplotlib 的简化数据集版本,并添加了分支长度

图 7.5 – 一个基于 matplotlib 的简化数据集版本,并添加了分支长度

  1. 现在我们将绘制完整的数据集,但每个树的部分将使用不同的颜色。如果一个子树只有单一的病毒种类,它将拥有自己独特的颜色。埃博拉病毒(EBOV)将有两种颜色,也就是说,一种用于 2014 年的疫情,另一种用于其他年份,如下所示:

    fig = plt.figure(figsize=(16, 22))
    ax = fig.add_subplot(111)
    from collections import OrderedDict
    my_colors = OrderedDict({
    'EBOV_2014': 'red',
    'EBOV': 'magenta',
    'BDBV': 'cyan',
    'SUDV': 'blue',
    'RESTV' : 'green',
    'TAFV' : 'yellow'
    })
    def get_color(name):
        for pref, color in my_colors.items():
            if name.find(pref) > -1:
                return color
        return 'grey'
    def color_tree(node, fun_color=get_color):
        if node.is_terminal():
            node.color = fun_color(node.name)
        else:
            my_children = set()
            for child in node.clades:
                color_tree(child, fun_color)
                my_children.add(child.color.to_hex())
            if len(my_children) == 1:
                node.color = child.color
            else:
                node.color = 'grey'
    ebola_color_tree = deepcopy(ebola_tree)
    color_tree(ebola_color_tree.root)
    Phylo.draw(ebola_color_tree, axes=ax, label_func=lambda x: x.name.split(' ')[0][1:] if x.name is not None else None)
    

这是一个树遍历算法,类似于前面例子中呈现的算法。作为一个递归算法,它的工作方式如下。如果节点是叶子,它将根据其种类(或 EBOV 疫情年份)来着色。如果它是一个内部节点,并且所有下面的后代节点都是同一物种,它将采用该物种的颜色;如果后代节点包含多个物种,它将被着色为灰色。实际上,颜色函数可以更改,并且稍后会进行更改。只有边缘颜色会被使用(标签会以黑色打印)。

注意,阶梯化(在前面的例子中使用 DendroPy 完成)对于清晰的视觉效果帮助很大。

我们还会对属树进行深拷贝,以便对副本进行着色;请记住,在前面的例子中提到的某些树遍历函数可能会改变状态,而在这种情况下,我们希望保留一个没有任何着色的版本。

注意使用了 lambda 函数来清理由 trimAl 修改的名称,如下图所示:

图 7.6 – 一个带有完整埃博拉病毒数据集的阶梯化和着色的系统发育树

图 7.6 – 一个带有完整埃博拉病毒数据集的阶梯化和着色的系统发育树

还有更多...

树和图的可视化是一个复杂的话题;可以说,这里的树可视化是严谨的,但远不漂亮。DendroPy 的一个替代方案是 ETE(etetoolkit.org/),它具有更多的可视化功能。绘制树和图的常见替代方案包括 Cytoscape(cytoscape.org/)和 Gephi(gephi.github.io/)。如果你想了解更多关于渲染树和图的算法,可以查看 Wikipedia 页面:en.wikipedia.org/wiki/Graph_drawing,了解这个迷人的话题。

不过,要小心不要以牺牲实质内容为代价追求风格。例如,本书的上一版使用图形渲染库绘制了一个美观的系统发育树。虽然它显然是该章节中最漂亮的图像,但在分支长度上却具有误导性。

第九章:使用蛋白质数据银行

蛋白质组学是研究蛋白质的学科,包括蛋白质的功能和结构。该领域的主要目标之一是表征蛋白质的三维结构。在蛋白质组学领域,最广为人知的计算资源之一是蛋白质数据银行PDB),这是一个包含大分子生物体结构数据的数据库。当然,也有许多数据库专注于蛋白质的初级结构;这些数据库与我们在第二章中看到的基因组数据库有些相似,了解 NumPy、pandas、Arrow 和 Matplotlib

在本章中,我们将主要关注如何处理来自 PDB 的数据。我们将学习如何解析 PDB 文件,执行一些几何计算,并可视化分子。我们将使用旧的 PDB 文件格式,因为从概念上讲,它允许你在一个稳定的环境中执行大多数必要的操作。话虽如此,新的 mmCIF 格式计划取代 PDB 格式,在使用 Biopython 解析 mmCIF 文件的食谱中也会介绍它。我们将使用 Biopython 并介绍 PyMOL 用于可视化。我们不会在这里讨论分子对接,因为那更适合一本关于化学信息学的书。

在本章中,我们将使用一个经典的蛋白质例子:肿瘤蛋白 p53,它参与细胞周期的调节(例如,凋亡)。该蛋白质与癌症关系密切。网上有大量关于该蛋白质的信息。

让我们从你现在应该更熟悉的内容开始:访问数据库,特别是获取蛋白质的初级结构(即氨基酸序列)。

在本章中,我们将介绍以下内容:

  • 在多个数据库中查找蛋白质

  • 介绍 Bio.PDB

  • 从 PDB 文件中提取更多信息

  • 在 PDB 文件中计算分子距离

  • 执行几何操作

  • 使用 PyMOL 进行动画制作

  • 使用 Biopython 解析 mmCIF 文件

在多个数据库中查找蛋白质

在我们开始进行更多的结构生物学工作之前,我们将看看如何访问现有的蛋白质组学数据库,比如 UniProt。我们将查询 UniProt 以查找我们感兴趣的基因,TP53,并从那里开始。

做好准备

为了访问数据,我们将使用 Biopython 和 REST API(我们在第五章中使用了类似的方法,基因组学工作)以及requests库来访问 Web API。requests API 是一个易于使用的 Web 请求封装库,可以通过标准的 Python 机制(例如,pipconda)安装。你可以在Chapter08/Intro.py笔记本文件中找到这部分内容。

如何实现...

请查看以下步骤:

  1. 首先,我们来定义一个函数来执行对 UniProt 的 REST 查询,代码如下:

    import requests
    server = 'http://www.uniprot.org/uniprot'
    def do_request(server, ID='', **kwargs):
        params = ''
        req = requests.get('%s/%s%s' % (server, ID, params), params=kwargs)
        if not req.ok:
            req.raise_for_status()
        return req
    
  2. 现在,我们可以查询所有已审阅的p53基因:

    req = do_request(server, query='gene:p53 AND reviewed:yes', format='tab',
     columns='id,entry name,length,organism,organism-id,database(PDB),database(HGNC)',
     limit='50')
    

我们将查询p53基因,并请求查看所有已审核的条目(即,手动校对过的)。输出将以表格格式显示。我们将请求最多 50 个结果,并指定所需的列。

我们本可以将输出限制为仅包含人类数据,但为了这个示例,我们将包括所有可用的物种。

  1. 让我们查看结果,内容如下:

    import pandas as pd
    import io
    uniprot_list = pd.read_table(io.StringIO(req.text))
    uniprot_list.rename(columns={'Organism ID': 'ID'}, inplace=True)
    print(uniprot_list)
    

我们使用pandas来轻松处理制表符分隔的列表并进行美观打印。笔记本的简化输出如下:

图 8.1 - 一个简化的 TP53 蛋白物种列表

图 8.1 - 一个简化的 TP53 蛋白物种列表

  1. 现在,我们可以获取人类p53基因 ID,并使用 Biopython 检索并解析SwissProt记录:

    from Bio import ExPASy, SwissProt
    p53_human = uniprot_list[
        (uniprot_list.ID == 9606) &
        (uniprot_list['Entry name'].str.contains('P53'))]['Entry'].iloc[0] 
    handle = ExPASy.get_sprot_raw(p53_human)
    sp_rec = SwissProt.read(handle)
    

然后,我们使用 Biopython 的SwissProt模块来解析记录。9606是人类的 NCBI 分类代码。

和往常一样,如果网络服务出现错误,可能是网络或服务器问题。如果是这样,请稍后再试。

  1. 让我们来看一下p53记录,内容如下:

    print(sp_rec.entry_name, sp_rec.sequence_length, sp_rec.gene_name)
    print(sp_rec.description)
    print(sp_rec.organism, sp_rec.seqinfo)
    print(sp_rec.sequence)
    print(sp_rec.comments)
    print(sp_rec.keywords)
    

输出如下:

P53_HUMAN 393 Name=TP53; Synonyms=P53;
 RecName: Full=Cellular tumor antigen p53; AltName: Full=Antigen NY-CO-13; AltName: Full=Phosphoprotein p53; AltName: Full=Tumor suppressor p53;
 Homo sapiens (Human). (393, 43653, 'AD5C149FD8106131')
 MEEPQSDPSVEPPLSQETFSDLWKLLPENNVLSPLPSQAMDDLMLSPDDIEQWFTED PGPDEAPRMPEAAPPVAPAPAAPTPAAPAPAPSWPLSSSVPSQKTYQGSYGFRLGF LHSGTAKSVTCTYSPALNKMFCQLAKTCPVQLWVDSTPPPGTRVRAMAIYKQSQHM TEVVRRCPHHERCSDSDGLAPPQHLIRVEGNLRVEYLDDRNTFRHSVVVPYEPPEVG SDCTTIHYNYMCNSSCMGGMNRRPILTIITLEDSSGNLLGRNSFEVRVCACPGRDRR TEEENLRKKGEPHHELPPGSTKRALPNNTSSSPQPKKKPLDGEYFTLQIRGRERFEM FRELNEALELKDAQAGKEPGGSRAHSSHLKSKKGQSTSRHKKLMFKTEGPDSD
  1. 更深入地查看前面的记录会发现许多非常有趣的信息,特别是在特征、cross_references方面:

    from collections import defaultdict
    done_features = set()
    print(len(sp_rec.features))
    for feature in sp_rec.features:
        if feature[0] in done_features:
            continue
        else:
            done_features.add(feature[0])
            print(feature)
    print(len(sp_rec.cross_references))
    per_source = defaultdict(list)
    for xref in sp_rec.cross_references:
        source = xref[0]
        per_source[source].append(xref[1:])
    print(per_source.keys())
    done_GOs = set()
    print(len(per_source['GO']))
    for annot in per_source['GO']:
        if annot[1][0] in done_GOs:
            continue
        else:
            done_GOs.add(annot[1][0])
            print(annot)
    

请注意,我们这里并没有打印所有的信息,仅仅是一个摘要。我们打印了序列的多个特征,每种类型展示一个例子,还有一些外部数据库的引用,以及提到的数据库,还有一些 GO 条目,并附上了三个例子。目前,仅这个蛋白质就有 1,509 个特征、923 个外部引用和 173 个 GO 术语。以下是输出的高度简化版本:

Total features: 1509
type: CHAIN
location: [0:393]
id: PRO_0000185703
qualifiers:
    Key: note, Value: Cellular tumor antigen p53
type: DNA_BIND
location: [101:292]
qualifiers:
type: REGION
location: [0:320]
qualifiers:
    Key: evidence, Value: ECO:0000269|PubMed:25732823
    Key: note, Value: Interaction with CCAR2
[...]
Cross references:  923
dict_keys(['EMBL', 'CCDS', 'PIR', 'RefSeq', 'PDB', 'PDBsum', 'BMRB', 'SMR', 'BioGRID', 'ComplexPortal', 'CORUM', 'DIP', 'ELM', 'IntAct', 'MINT', 'STRING', 'BindingDB', 'ChEMBL', 'DrugBank', 'MoonDB', 'TCDB', 'GlyGen', 'iPTMnet', 'MetOSite', 'PhosphoSitePlus', 'BioMuta', 'DMDM', 'SWISS-2DPAGE', 'CPTAC', 'EPD', 'jPOST', 'MassIVE', 'MaxQB', 'PaxDb', 'PeptideAtlas', 'PRIDE', 'ProteomicsDB', 'ABCD', 'Antibodypedia', 'CPTC', 'DNASU', 'Ensembl', 'GeneID', 'KEGG', 'MANE-Select', 'UCSC', 'CTD', 'DisGeNET', 'GeneCards', 'GeneReviews', 'HGNC', 'HPA', 'MalaCards', 'MIM', 'neXtProt', 'OpenTargets', 'Orphanet', 'PharmGKB', 'VEuPathDB', 'eggNOG', 'GeneTree', 'InParanoid', 'OMA', 'OrthoDB', 'PhylomeDB', 'TreeFam', 'PathwayCommons', 'Reactome', 'SABIO-RK', 'SignaLink', 'SIGNOR', 'BioGRID-ORCS', 'ChiTaRS', 'EvolutionaryTrace', 'GeneWiki', 'GenomeRNAi', 'Pharos', 'PRO', 'Proteomes', 'RNAct', 'Bgee', 'ExpressionAtlas', 'Genevisible', 'GO', 'CDD', 'DisProt', 'Gene3D', 'IDEAL', 'InterPro', 'PANTHER', 'Pfam', 'PRINTS', 'SUPFAM', 'PROSITE'])
Annotation SOURCES: 173
('GO:0005813', 'C:centrosome', 'IDA:UniProtKB')
('GO:0036310', 'F:ATP-dependent DNA/DNA annealing activity', 'IDA:UniProtKB')
('GO:0006914', 'P:autophagy', 'IMP:CAFA')

更多内容

还有更多关于蛋白质的信息数据库——其中一些在前面的记录中已有提到。你可以探索其结果,尝试在其他地方查找数据。有关 UniProt REST 接口的详细信息,请参考www.uniprot.org/help/programmatic_access

介绍 Bio.PDB

在这里,我们将介绍 Biopython 的PDB模块,用于处理 PDB 文件。我们将使用三个模型,这些模型代表p53蛋白的部分结构。你可以在www.rcsb.org/pdb/101/motm.do?momID=31了解更多关于这些文件和p53的信息。

准备工作

你应该已经了解了基本的PDB数据模型,包括模型、链、残基和原子对象。关于Biopython 的结构生物信息学 FAQ的详细解释可以在biopython.org/wiki/The_Biopython_Structural_Bioinformatics_FAQ找到。

你可以在Chapter08/PDB.py笔记本文件中找到这些内容。

在我们将要下载的三个模型中,1TUP模型将用于接下来的所有示例。花点时间研究这个模型,它将在后续帮助你。

如何操作……

请看以下步骤:

  1. 首先,让我们检索我们感兴趣的模型,如下所示:

    from Bio import PDB
    repository = PDB.PDBList()
    repository.retrieve_pdb_file('1TUP', pdir='.', file_format='pdb')
    repository.retrieve_pdb_file('1OLG', pdir='.', file_format='pdb')
    repository.retrieve_pdb_file('1YCQ', pdir='.', file_format='pdb')
    

请注意,Bio.PDB会为您下载文件。此外,只有在没有本地副本的情况下才会进行这些下载。

  1. 让我们解析我们的记录,如下所示的代码:

    parser = PDB.PDBParser()
    p53_1tup = parser.get_structure('P 53 - DNA Binding', 'pdb1tup.ent')
    p53_1olg = parser.get_structure('P 53 - Tetramerization', 'pdb1olg.ent')
    p53_1ycq = parser.get_structure('P 53 - Transactivation', 'pdb1ycq.ent')
    

您可能会收到有关文件内容的一些警告。这些通常不会有问题。

  1. 让我们检查我们的头文件,如下所示:

    def print_pdb_headers(headers, indent=0):
       ind_text = ' ' * indent
       for header, content in headers.items():
           if type(content) == dict:
              print('\n%s%20s:' % (ind_text, header))
              print_pdb_headers(content, indent + 4)
              print()
           elif type(content) == list:
              print('%s%20s:' % (ind_text, header))
              for elem in content:
                  print('%s%21s %s' % (ind_text, '->', elem))
          else:
              print('%s%20s: %s' % (ind_text, header, content))
    print_pdb_headers(p53_1tup.header)
    

头文件被解析为字典的字典。因此,我们将使用递归函数来解析它们。此函数将增加缩进以便于阅读,并使用->前缀注释元素列表。有关递归函数的示例,请参见前一章,第七章系统发生学。有关 Python 中递归的高级讨论,请转到最后一章,第十二章生物信息学的函数式编程。简化后的输出如下所示:

                name: tumor suppressor p53 complexed with dna
                head: antitumor protein/dna
              idcode: 1TUP
     deposition_date: 1995-07-11
        release_date: 1995-07-11
    structure_method: x-ray diffraction
          resolution: 2.2
 structure_reference:
-> n.p.pavletich,k.a.chambers,c.o.pabo the dna-binding domain of p53 contains the four conserved regions and the major mutation hot spots genes dev. v. 7 2556 1993 issn 0890-9369 
              author: Y.Cho,S.Gorina,P.D.Jeffrey,N.P.Pavletich
            compound:
                       2:
misc: 
molecule: dna (5'-d(*ap*tp*ap*ap*tp*tp*gp*gp*gp*cp*ap*ap*gp*tp*cp*tp*a p*gp*gp*ap*a)-3') 
                       chain: f
                  engineered: yes
has_missing_residues: True
    missing_residues:
                   -> {'model': None, 'res_name': 'ARG', 'chain': 'A', 'ssseq': 290, 'insertion': None}
keywords: antigen p53, antitumor protein/dna complex
             journal: AUTH   Y.CHO,S.GORINA,P.D.JEFFREY,N.P.PAVLETICHTITL   CRYSTAL STRUCTURE OF A P53 TUMOR SUPPRESSOR-DNATITL 2 COMPLEX: UNDERSTANDING TUMORIGENIC MUTATIONS.REF    SCIENCE57
  1. 我们想要知道这些文件中每条链的内容;为此,让我们看一下COMPND记录:

    print(p53_1tup.header['compound'])
    print(p53_1olg.header['compound'])
    print(p53_1ycq.header['compound'])
    

这将打印出在前面的代码中列出的所有化合物头文件。不幸的是,这不是获取链信息的最佳方式。另一种方法是获取DBREF记录,但 Biopython 的解析器目前无法访问这些记录。话虽如此,使用诸如grep之类的工具将轻松提取这些信息。

注意,对于1TUP模型,链ABC来自蛋白质,而链EF来自 DNA。这些信息将在未来很有用。

  1. 让我们对每个PDB文件进行自上而下的分析。现在,让我们只获取所有的链、残基数和每条链中的原子数,如下所示:

    def describe_model(name, pdb):
    print()
    for model in pdb:
        for chain in model:
            print('%s - Chain: %s. Number of residues: %d. Number of atoms: %d.' %
                  (name, chain.id, len(chain), len(list(chain.get_atoms()))))
    describe_model('1TUP', p53_1tup)
    describe_model('1OLG', p53_1olg)
    describe_model('1YCQ', p53_1ycq)
    

在稍后的配方中,我们将采用自下而上的方法。以下是1TUP的输出:

1TUP - Chain: E. Number of residues: 43\. Number of atoms: 442.
1TUP - Chain: F. Number of residues: 35\. Number of atoms: 449.
1TUP - Chain: A. Number of residues: 395\. Number of atoms: 1734.
1TUP - Chain: B. Number of residues: 265\. Number of atoms: 1593.
1TUP - Chain: C. Number of residues: 276\. Number of atoms: 1610.

1OLG - Chain: A. Number of residues: 42\. Number of atoms: 698.
1OLG - Chain: B. Number of residues: 42\. Number of atoms: 698.
1OLG - Chain: C. Number of residues: 42\. Number of atoms: 698.
1OLG - Chain: D. Number of residues: 42\. Number of atoms: 698.

1YCQ - Chain: A. Number of residues: 123\. Number of atoms: 741.
1YCQ - Chain: B. Number of residues: 16\. Number of atoms: 100.
  1. 让我们获取所有非标准残基(HETATM),除了水,在1TUP模型中,如下所示的代码:

    for residue in p53_1tup.get_residues():
        if residue.id[0] in [' ', 'W']:
            continue
    print(residue.id)
    

我们有三个锌原子,每个蛋白链一个。

  1. 让我们来看一个残基:

    res = next(p53_1tup[0]['A'].get_residues())
    print(res)
    for atom in res:
        print(atom, atom.serial_number, atom.element)
    p53_1tup[0]['A'][94]['CA']
    

这将打印出某个残基中的所有原子:

<Residue SER het=  resseq=94 icode= >
 <Atom N> 858 N
 <Atom CA> 859 C
 <Atom C> 860 C
 <Atom O> 861 O
 <Atom CB> 862 C
 <Atom OG> 863 O
 <Atom CA>

注意最后一句话。它只是为了向您展示,您可以通过解析模型、链、残基和最终原子来直接访问一个原子。

  1. 最后,让我们将蛋白质片段导出到一个 FASTA 文件中,如下所示:

    from Bio.SeqIO import PdbIO, FastaIO
    def get_fasta(pdb_file, fasta_file, transfer_ids=None):
        fasta_writer = FastaIO.FastaWriter(fasta_file)
        fasta_writer.write_header()
        for rec in PdbIO.PdbSeqresIterator(pdb_file):
            if len(rec.seq) == 0:
                continue
            if transfer_ids is not None and rec.id not in transfer_ids:
                continue
            print(rec.id, rec.seq, len(rec.seq))
            fasta_writer.write_record(rec)
    
    get_fasta(open('pdb1tup.ent'), open('1tup.fasta', 'w'), transfer_ids=['1TUP:B'])
    get_fasta(open('pdb1olg.ent'), open('1olg.fasta', 'w'), transfer_ids=['1OLG:B'])
    get_fasta(open('pdb1ycq.ent'), open('1ycq.fasta', 'w'), transfer_ids=['1YCQ:B'])
    

如果您检查蛋白质链,您会发现它们在每个模型中都是相等的,因此我们导出一个单独的链。在1YCQ的情况下,我们导出最小的一个,因为最大的一个与p53无关。正如您在这里看到的,我们使用的是Bio.SeqIO,而不是Bio.PDB

还有更多

PDB 解析器不完整。很可能不会很快见到完整的解析器,因为社区正在迁移到 mmCIF 格式。

尽管未来是 mmCIF 格式(mmcif.wwpdb.org/),PDB 文件仍然存在。从概念上讲,文件解析后的许多操作是类似的。

从 PDB 文件中提取更多信息

在这里,我们将继续探索Bio.PDB从 PDB 文件生成的记录结构。

准备工作

有关我们正在使用的 PDB 模型的详细信息,请参见前面的章节。

你可以在Chapter08/Stats.py Notebook 文件中找到这些内容。

如何操作…

我们将通过以下步骤开始:

  1. 首先,让我们提取1TUP,如下所示:

    from Bio import PDB
    repository = PDB.PDBList()
    parser = PDB.PDBParser()
    repository.retrieve_pdb_file('1TUP', pdir='.', file_format='pdb') p53_1tup = parser.get_structure('P 53', 'pdb1tup.ent')
    
  2. 然后,提取一些与原子相关的统计信息:

    from collections import defaultdict
    atom_cnt = defaultdict(int)
    atom_chain = defaultdict(int)
    atom_res_types = defaultdict(int)
    for atom in p53_1tup.get_atoms():
        my_residue = atom.parent
        my_chain = my_residue.parent
        atom_chain[my_chain.id] += 1
        if my_residue.resname != 'HOH':
            atom_cnt[atom.element] += 1
        atom_res_types[my_residue.resname] += 1
    print(dict(atom_res_types))
    print(dict(atom_chain))
    print(dict(atom_cnt))
    

这将打印出原子残基类型、每条链的原子数量和每种元素的数量,如下所示:

{' DT': 257, ' DC': 152, ' DA': 270, ' DG': 176, 'HOH': 384, 'SER': 323, 'VAL': 315, 'PRO': 294, 'GLN': 189, 'LYS': 135, 'THR': 294, 'TYR': 288, 'GLY': 156, 'PHE': 165, 'ARG': 561, 'LEU': 336, 'HIS': 210, 'ALA': 105, 'CYS': 180, 'ASN': 216, 'MET': 144, 'TRP': 42, 'ASP': 192, 'ILE': 144, 'GLU': 297, ' ZN': 3}
 {'E': 442, 'F': 449, 'A': 1734, 'B': 1593, 'C': 1610}
 {'O': 1114, 'C': 3238, 'N': 1001, 'P': 40, 'S': 48, 'ZN': 3}

请注意,前面提到的残基数不是正确的残基数,而是某个残基类型被引用的次数(它加起来等于原子数,而不是残基数)。

注意水(W)、核苷酸(DADCDGDT)和锌(ZN)残基,它们与氨基酸残基一起出现。

  1. 现在,让我们统计每个残基的实例数量和每条链的残基数量:

    res_types = defaultdict(int)
    res_per_chain = defaultdict(int)
    for residue in p53_1tup.get_residues():
    res_types[residue.resname] += 1
    res_per_chain[residue.parent.id] +=1
    print(dict(res_types))
    print(dict(res_per_chain))
    

以下是输出结果:

{' DT': 13, ' DC': 8, ' DA': 13, ' DG': 8, 'HOH': 384, 'SER': 54, 'VAL': 45, 'PRO': 42, 'GLN': 21, 'LYS': 15, 'THR': 42, 'TYR': 24, 'GLY': 39, 'PHE': 15, 'ARG': 51, 'LEU': 42, 'HIS': 21, 'ALA': 21, 'CYS': 30, 'ASN': 27, 'MET': 18, 'TRP': 3, 'ASP': 24, 'ILE': 18, 'GLU': 33, ' ZN': 3}
 {'E': 43, 'F': 35, 'A': 395, 'B': 265, 'C': 276}
  1. 我们还可以获取一组原子的边界:

    import sys
    def get_bounds(my_atoms):
        my_min = [sys.maxsize] * 3
        my_max = [-sys.maxsize] * 3
        for atom in my_atoms:
            for i, coord in enumerate(atom.coord):
                if coord < my_min[i]:
                    my_min[i] = coord
                if coord > my_max[i]:
                    my_max[i] = coord
        return my_min, my_max
    chain_bounds = {}
    for chain in p53_1tup.get_chains():
        print(chain.id, get_bounds(chain.get_atoms()))
        chain_bounds[chain.id] = get_bounds(chain.get_atoms())
    print(get_bounds(p53_1tup.get_atoms()))
    

一组原子可以是整个模型、一条链、一种残基或任何你感兴趣的子集。在这种情况下,我们将打印所有链条和整个模型的边界。数字表达不太直观,因此我们将采用更具图形化的方式。

  1. 为了了解每条链的大小,绘图可能比前面代码中的数字更具信息量:

    import matplotlib.pyplot as plt
    from mpl_toolkits.mplot3d import Axes3D
    fig = plt.figure(figsize=(16, 9))
    ax3d = fig.add_subplot(111, projection='3d')
    ax_xy = fig.add_subplot(331)
    ax_xy.set_title('X/Y')
    ax_xz = fig.add_subplot(334)
    ax_xz.set_title('X/Z')
    ax_zy = fig.add_subplot(337)
    ax_zy.set_title('Z/Y')
    color = {'A': 'r', 'B': 'g', 'C': 'b', 'E': '0.5', 'F': '0.75'}
    zx, zy, zz = [], [], []
    for chain in p53_1tup.get_chains():
        xs, ys, zs = [], [], []
        for residue in chain.get_residues():
            ref_atom = next(residue.get_iterator())
            x, y, z = ref_atom.coord
            if ref_atom.element == 'ZN':
                zx.append(x)
                zy.append(y)
                zz.append(z)
                continue
            xs.append(x)
            ys.append(y)
            zs.append(z)
        ax3d.scatter(xs, ys, zs, color=color[chain.id])
        ax_xy.scatter(xs, ys, marker='.', color=color[chain.id])
        ax_xz.scatter(xs, zs, marker='.', color=color[chain.id])
        ax_zy.scatter(zs, ys, marker='.', color=color[chain.id])
    ax3d.set_xlabel('X')
    ax3d.set_ylabel('Y')
    ax3d.set_zlabel('Z')
    ax3d.scatter(zx, zy, zz, color='k', marker='v', s=300)
    ax_xy.scatter(zx, zy, color='k', marker='v', s=80)
    ax_xz.scatter(zx, zz, color='k', marker='v', s=80)
    ax_zy.scatter(zz, zy, color='k', marker='v', s=80)
    for ax in [ax_xy, ax_xz, ax_zy]:
        ax.get_yaxis().set_visible(False)
        ax.get_xaxis().set_visible(False)
    

目前有很多分子可视化工具。实际上,我们稍后会讨论 PyMOL。不过,matplotlib对于简单的可视化已经足够了。关于matplotlib最重要的一点是它非常稳定,且容易集成到可靠的生产代码中。

在下图中,我们对链条进行了三维绘制,DNA 部分为灰色,蛋白质链条用不同颜色表示。我们还在下图的左侧绘制了平面投影(X/YX/Z,和Z/Y):

图 8.2 - 蛋白质链的空间分布——主图是一个 3D 图,左侧子图是平面视图(X/Y,X/Z,和 Z/Y)

图 8.2 - 蛋白质链的空间分布——主图是一个 3D 图,左侧子图是平面视图(X/Y,X/Z,和 Z/Y)

计算 PDB 文件中的分子距离

在这里,我们将找到与1TUP模型中三个锌原子接近的原子。我们将考虑这些锌原子之间的几种距离,并借此机会讨论算法的性能。

准备工作

你可以在Chapter08/Distance.py Notebook 文件中找到这些内容。

如何操作…

请查看以下步骤:

  1. 让我们加载我们的模型,如下所示:

    from Bio import PDB
    repository = PDB.PDBList()
    parser = PDB.PDBParser()
    repository.retrieve_pdb_file('1TUP', pdir='.', file_format='pdb')
    p53_1tup = parser.get_structure('P 53', 'pdb1tup.ent')
    
  2. 现在,我们来提取锌原子,后续我们将以这些原子为基准进行比较:

    zns = []for atom in p53_1tup.get_atoms():
    if atom.element == 'ZN':
    zns.append(atom)
    for zn in zns:
        print(zn, zn.coord)
    

你应该能看到三个锌原子。

  1. 现在,让我们定义一个函数来获取一个原子与一组其他原子之间的距离,如下所示:

    import math
    def get_closest_atoms(pdb_struct, ref_atom, distance):
        atoms = {}
        rx, ry, rz = ref_atom.coord
        for atom in pdb_struct.get_atoms():
            if atom == ref_atom:
                continue
            x, y, z = atom.coord
            my_dist = math.sqrt((x - rx)**2 + (y - ry)**2 + (z - rz)**2)
            if my_dist < distance:
                atoms[atom] = my_dist
        return atoms
    

我们获取参考原子的坐标,然后遍历我们希望比较的原子列表。如果某个原子足够接近,它会被添加到return列表中。

  1. 现在我们计算接近锌原子的原子,距离在我们的模型中可以达到 4 埃(Å):

    for zn in zns:
        print()
        print(zn.coord)
        atoms = get_closest_atoms(p53_1tup, zn, 4)
        for atom, distance in atoms.items():
            print(atom.element, distance, atom.coord)
    

这里,我们展示了第一个锌原子的结果,包括元素、距离和坐标:

[58.108 23.242 57.424]
 C 3.4080117696286854 [57.77  21.214 60.142]
 S 2.3262243799594877 [57.065 21.452 58.482]
 C 3.4566537492335123 [58.886 20.867 55.036]
 C 3.064120559761192 [58.047 22.038 54.607]
 N 1.9918273537290707 [57.755 23.073 55.471]
 C 2.9243719601324525 [56.993 23.943 54.813]
 C 3.857729198122736 [61.148 25.061 55.897]
 C 3.62725094648044 [61.61  24.087 57.001]
 S 2.2789209624943494 [60.317 23.318 57.979]
 C 3.087214470667822 [57.205 25.099 59.719]
 S 2.2253158446520818 [56.914 25.054 57.917]

我们只有三个锌原子,因此计算量大大减少。然而,假设我们有更多原子,或者我们在整个原子集之间进行成对比较(记住,在成对比较的情况下,比较次数是随着原子数量的平方增长的)。虽然我们的案例较小,但预测使用案例并不难,更多的比较会消耗大量时间。我们很快会回到这个问题。

  1. 让我们看看随着距离增加,我们会得到多少原子:

    for distance in [1, 2, 4, 8, 16, 32, 64, 128]:
        my_atoms = []
        for zn in zns:
            atoms = get_closest_atoms(p53_1tup, zn, distance)
            my_atoms.append(len(atoms))
        print(distance, my_atoms)
    

结果如下:

1 [0, 0, 0]
2 [1, 0, 0]
4 [11, 11, 12]
8 [109, 113, 106]
16 [523, 721, 487]
32 [2381, 3493, 2053]
64 [5800, 5827, 5501]
128 [5827, 5827, 5827]
  1. 如我们之前所见,这个特定的情况并不太昂贵,但我们还是计时看看:

    import timeit
    nexecs = 10
    print(timeit.timeit('get_closest_atoms(p53_1tup, zns[0], 4.0)',
          'from __main__ import get_closest_atoms, p53_1tup, zns',
          number=nexecs) / nexecs * 1000)
    

在这里,我们将使用timeit模块执行这个函数 10 次,然后以毫秒为单位打印结果。我们将函数作为字符串传递,并传递另一个包含必要导入的字符串以使函数正常工作。在 Notebook 中,你可能知道%timeit魔法命令,它能让你的生活变得更加轻松。在测试代码的机器上,这大约需要 40 毫秒。显然,在你的电脑上,你会得到稍有不同的结果。

  1. 我们能做得更好吗?让我们考虑一个不同的distance函数,如下面的代码所示:

    def get_closest_alternative(pdb_struct, ref_atom, distance):
        atoms = {}
        rx, ry, rz = ref_atom.coord
        for atom in pdb_struct.get_atoms():
            if atom == ref_atom:
                continue
            x, y, z = atom.coord
            if abs(x - rx) > distance or abs(y - ry) > distance or abs(z - rz) > distance:
                continue
            my_dist = math.sqrt((x - rx)**2 + (y - ry)**2 + (z - rz)**2)
            if my_dist < distance:
                atoms[atom] = my_dist
        return atoms
    

所以,我们对原始函数进行修改,加入一个非常简单的if条件来处理距离。这样做的理由是,平方根计算和可能的浮点幂运算非常昂贵,因此我们将尽量避免它。然而,对于任何维度中距离小于目标距离的原子,这个函数会变得更加昂贵。

  1. 现在,让我们来计时:

    print(timeit.timeit('get_closest_alternative(p53_1tup, zns[0], 4.0)',
          'from __main__ import get_closest_alternative, p53_1tup, zns',
          number=nexecs) / nexecs * 1000)
    

在我们之前使用的同一台机器上,它需要 16 毫秒,这意味着它大约快了三倍。

  1. 然而,这总是更好吗?让我们比较不同距离下的成本,如下所示:

    print('Standard')
    for distance in [1, 4, 16, 64, 128]:
        print(timeit.timeit('get_closest_atoms(p53_1tup, zns[0], distance)',
              'from __main__ import get_closest_atoms, p53_1tup, zns, distance',
              number=nexecs) / nexecs * 1000)
    print('Optimized')
    for distance in [1, 4, 16, 64, 128]:
        print(timeit.timeit('get_closest_alternative(p53_1tup, zns[0], distance)',
              'from __main__ import get_closest_alternative, p53_1tup, zns, distance',
              number=nexecs) / nexecs * 1000)
    

结果显示在以下输出中:

Standard
 85.08649739999328
 86.50681579999855
 86.79630599999655
 96.95437099999253
 96.21982420001132
 Optimized
 30.253444099980698
 32.69531210000878
 52.965772600009586
 142.53310030001103
 141.26269519999823

请注意,标准版本的成本大致是恒定的,而优化版本的成本则取决于最近原子的距离;距离越大,使用额外if和平方根进行计算的情况就越多,从而使得函数变得更昂贵。

这里的关键点是,你可能可以通过聪明的计算捷径编写更高效的函数,但复杂度成本可能会发生质的变化。在前面的例子中,我建议第二个函数在所有现实且有趣的情况下更高效,特别是在你尝试找到最接近的原子时。然而,在设计你自己的优化算法时,你必须小心。

执行几何操作

现在我们将使用几何信息进行计算,包括计算链条和整个模型的质心。

准备工作

你可以在 Chapter08/Mass.py 笔记本文件中找到这些内容。

如何操作...

让我们看一下以下步骤:

  1. 首先,让我们获取数据:

    from Bio import PDB
    repository = PDB.PDBList()
    parser = PDB.PDBParser()
    repository.retrieve_pdb_file('1TUP', pdir='.', file_format='pdb')
    p53_1tup = parser.get_structure('P 53', 'pdb1tup.ent')
    
  2. 然后,使用以下代码回顾我们拥有的残基类型:

    my_residues = set()
    for residue in p53_1tup.get_residues():
        my_residues.add(residue.id[0])
    print(my_residues)
    

所以,我们有 H_ ZN(锌)和 W(水),它们是 HETATM 类型;绝大多数是标准 PDB 原子。

  1. 使用以下代码计算所有链条、锌和水的质量:

    def get_mass(atoms, accept_fun=lambda atom: atom.parent.id[0] != 'W'):
        return sum([atom.mass for atom in atoms if accept_fun(atom)])
    chain_names = [chain.id for chain in p53_1tup.get_chains()]
    my_mass = np.ndarray((len(chain_names), 3))
    for i, chain in enumerate(p53_1tup.get_chains()):
        my_mass[i, 0] = get_mass(chain.get_atoms())
        my_mass[i, 1] = get_mass(chain.get_atoms(),
            accept_fun=lambda atom: atom.parent.id[0] not in [' ', 'W'])
        my_mass[i, 2] = get_mass(chain.get_atoms(),
            accept_fun=lambda atom: atom.parent.id[0] == 'W')
    masses = pd.DataFrame(my_mass, index=chain_names, columns=['No Water','Zincs', 'Water'])
    print(masses)
    

get_mass 函数返回通过接收标准函数筛选的列表中所有原子的质量。这里,默认的接收标准是排除水分子残基。

然后,我们计算所有链条的质量。我们有三个版本:只有氨基酸、锌和水。锌仅在此模型中检测每条链中的单个原子。输出结果如下:

图 8.3 - 所有蛋白质链的质量

图 8.3 - 所有蛋白质链的质量

  1. 让我们计算模型的几何中心和质心,如下所示:

    def get_center(atoms,
        weight_fun=lambda atom: 1 if atom.parent.id[0] != 'W' else 0):
        xsum = ysum = zsum = 0.0
        acum = 0.0
        for atom in atoms:
            x, y, z = atom.coord
            weight = weight_fun(atom)
            acum += weight
            xsum += weight * x
            ysum += weight * y
            zsum += weight * z
        return xsum / acum, ysum / acum, zsum / acum
    print(get_center(p53_1tup.get_atoms()))
    print(get_center(p53_1tup.get_atoms(),
        weight_fun=lambda atom: atom.mass if atom.parent.id[0] != 'W' else 0))
    

首先,我们定义一个加权函数来获取质心的坐标。默认的函数会将所有原子视为相等,只要它们不是水分子残基。

然后,我们通过重新定义 weight 函数,将每个原子的值设置为其质量,来计算几何中心和质心。几何中心的计算不考虑分子量。

例如,你可能想要计算没有 DNA 链的蛋白质的质心。

  1. 让我们计算每个链条的质心和几何中心,如下所示:

    my_center = np.ndarray((len(chain_names), 6))
    for i, chain in enumerate(p53_1tup.get_chains()):
        x, y, z = get_center(chain.get_atoms())
        my_center[i, 0] = x
        my_center[i, 1] = y
        my_center[i, 2] = z
        x, y, z = get_center(chain.get_atoms(),
            weight_fun=lambda atom: atom.mass if atom.parent.id[0] != 'W' else 0)
        my_center[i, 3] = x
        my_center[i, 4] = y
        my_center[i, 5] = z
    weights = pd.DataFrame(my_center, index=chain_names,
        columns=['X', 'Y', 'Z', 'X (Mass)', 'Y (Mass)', 'Z (Mass)'])
    print(weights)
    

结果如图所示:

图 8.4 - 每个蛋白质链的质心和几何中心

图 8.4 - 每个蛋白质链的质心和几何中心

还有更多

虽然这本书不是基于蛋白质结构解析技术的,但需要记住的是,X 射线晶体学方法无法检测氢原子,因此残基的质量计算可能基于非常不准确的模型;有关更多信息,请参阅 www.umass.edu/microbio/chime/pe_beta/pe/protexpl/help_hyd.htm

使用 PyMOL 动画

在这里,我们将创建一个关于 p53 1TUP 模型的视频。为此,我们将使用 PyMOL 可视化库。我们将通过移动 p53 1TUP 模型开始动画,然后进行缩放;随着缩放,我们改变渲染策略,以便可以更深入地观察模型。你可以在 odysee.com/@Python:8/protein_video:8 找到你将生成的视频版本。

准备工作

这个配方将以 Python 脚本的形式呈现,而不是以 Notebook 的形式。这主要是因为输出不是交互式的,而是一组需要进一步后期处理的图像文件。

你需要安装 PyMOL(www.pymol.org)。在 Debian、Ubuntu 或 Linux 系统中,你可以使用apt-get install pymol命令。如果你使用的是 Conda,我建议不要使用它,因为依赖项会很容易解决——而且你将安装一个仅限 30 天试用的版本,需要许可证,而上述版本是完全开源的。如果你不是使用 Debian 或 Linux,我建议你安装适用于你操作系统的开源版本。

PyMOL 更多是一个交互式程序,而非 Python 库,因此我强烈建议你在继续操作前先进行一些探索。这可以是很有趣的!这个配方的代码可以在 GitHub 仓库中找到,作为脚本文件以及本章的 Notebook 文件,位于Chapter08。我们将在这个配方中使用PyMol_Movie.py文件。

如何操作...

请查看以下步骤:

  1. 让我们初始化并获取我们的 PDB 模型,并准备渲染,如下所示:

    import pymol
    from pymol import cmd
    #pymol.pymol_argv = ['pymol', '-qc'] # Quiet / no GUI
    pymol.finish_launching()
    cmd.fetch('1TUP', async=False)
    cmd.disable('all')
    cmd.enable('1TUP')
    cmd.hide('all')
    cmd.show('sphere', 'name zn')
    

请注意,pymol_argv这一行会使代码保持静默。在第一次执行时,你可能想要注释掉这行代码,看看用户界面。

对于电影渲染,这将非常有用(如我们将很快看到的)。作为一个库,PyMOL 的使用相当复杂。例如,导入后,你需要调用finish_launching。接着,我们获取我们的 PDB 文件。

接下来是一些 PyMOL 命令。许多关于交互式使用的网页指南对于理解发生的事情非常有帮助。在这里,我们将启用所有模型以供查看,隐藏所有模型(因为默认视图是线条表示,这样不够好),然后将锌渲染为球体。

在这个阶段,除锌外,其他都不可见。

  1. 为了渲染我们的模型,我们将使用以下三个场景:

    cmd.show('surface', 'chain A+B+C')
    cmd.show('cartoon', 'chain E+F')
    cmd.scene('S0', action='store', view=0, frame=0, animate=-1)
    cmd.show('cartoon')
    cmd.hide('surface')
    cmd.scene('S1', action='store', view=0, frame=0, animate=-1)
    cmd.hide('cartoon', 'chain A+B+C')
    cmd.show('mesh', 'chain A')
    cmd.show('sticks', 'chain A+B+C')
    cmd.scene('S2', action='store', view=0, frame=0, animate=-1)
    

我们需要定义两个场景。一个场景对应于我们围绕蛋白质移动(基于表面,因此是透明的),另一个场景则对应于我们深入观察(基于卡通式)。DNA 始终以卡通形式渲染。

我们还定义了第三个场景,当我们在最后进行缩小时使用。蛋白质将被渲染为棒状,并且我们将给 A 链添加一个网格,以便更清楚地展示它与 DNA 的关系。

  1. 让我们定义视频的基本参数,如下所示:

    cmd.set('ray_trace_frames', 0)
    cmd.mset(1, 500)
    

我们定义了默认的光线追踪算法。这一行并非必需,但请尝试将数字增加到123,并准备好等待很长时间。

如果你打开了 OpenGL 接口(即图形界面),那么只能使用0,因此对于这个快速版本,你需要打开 GUI(pymol_argv应该保持注释状态)。

然后,我们通知 PyMOL 我们将使用 500 帧。

  1. 在前 150 帧中,我们使用初始场景围绕蛋白质移动。我们稍微环绕模型,然后使用以下代码接近 DNA。

    cmd.frame(0)
    cmd.scene('S0')
    cmd.mview()
    cmd.frame(60)
    cmd.set_view((-0.175534308,   -0.331560850,   -0.926960170,
                 0.541812420,     0.753615797,   -0.372158051,
                 0.821965039,    -0.567564785,    0.047358301,
                 0.000000000,     0.000000000, -249.619018555,
                 58.625568390,   15.602619171,   77.781631470,
                 196.801528931, 302.436492920,  -20.000000000))
    cmd.mview()
    cmd.frame(90)
    cmd.set_view((-0.175534308,   -0.331560850,   -0.926960170,
                  0.541812420,    0.753615797,   -0.372158051,
                  0.821965039,   -0.567564785,    0.047358301,
                  -0.000067875,    0.000017881, -249.615447998,
                  54.029174805,   26.956727982,   77.124832153,
                 196.801528931,  302.436492920,  -20.000000000))
    cmd.mview()
    cmd.frame(150)
    cmd.set_view((-0.175534308,   -0.331560850,   -0.926960170,
                  0.541812420,    0.753615797,   -0.372158051,
                  0.821965039,   -0.567564785,    0.047358301,
                  -0.000067875,    0.000017881,  -55.406421661,
                  54.029174805,   26.956727982,   77.124832153,
                  2.592475891,  108.227416992,  -20.000000000))
    cmd.mview()
    

我们定义了三个点;前两个点与 DNA 对齐,最后一个点进入其中。我们通过在交互模式下使用 PyMOL,使用鼠标和键盘导航,并使用 get_view 命令来获取坐标(所有这些数字),然后可以剪切并粘贴。

第一个帧如下所示:

图 8.5 - 第 0 帧和场景 DS0

图 8.5 - 第 0 帧和场景 DS0

  1. 我们现在更改场景,为进入蛋白质内部做准备:

    cmd.frame(200)
    cmd.scene('S1')
    cmd.mview()
    

以下截图显示了当前的位置:

图 8.6 - DNA 分子附近的第 200 帧和场景 S1

图 8.6 - DNA 分子附近的第 200 帧和场景 S1

  1. 我们进入蛋白质内部,并在结束时使用以下代码更改场景:

    cmd.frame(350)
    cmd.scene('S1')
    cmd.set_view((0.395763457,   -0.173441306,    0.901825786,
                  0.915456235,    0.152441502,   -0.372427106,
                 -0.072881661,    0.972972929,    0.219108686,
                  0.000070953,    0.000013039,  -37.689743042,
                 57.748500824,   14.325904846,   77.241867065,
                 -15.123448372,   90.511535645,  -20.000000000))
    cmd.mview()
    cmd.frame(351)
    cmd.scene('S2')
    cmd.mview()
    

我们现在完全进入其中,如以下截图所示:

图 8.7 - 第 350 帧 - 场景 S1 即将更改为 S2

图 8.7 - 第 350 帧 - 场景 S1 即将更改为 S2

  1. 最后,我们让 PyMOL 返回到原始位置,然后播放、保存并退出:

    cmd.frame(500)
    cmd.scene('S2')
    cmd.mview()
    cmd.mplay()
    cmd.mpng('p53_1tup')
    cmd.quit()
    

这将生成 500 个以 p53_1tup 为前缀的 PNG 文件。

这是一个接近结束的帧(450):

图 8.8 - 第 450 帧和场景 S2

图 8.8 - 第 450 帧和场景 S2

还有更多

该 YouTube 视频是在 Linux 上使用 ffmpeg 以每秒 15 帧的速度生成的,如下所示:

ffmpeg -r 15 -f image2 -start_number 1 -i "p53_1tup%04d.png" example.mp4

有很多应用程序可以用来从图像生成视频。PyMOL 可以生成 MPEG 格式的视频,但需要安装额外的库。

PyMOL 是为从其控制台交互式使用而创建的(可以在 Python 中扩展)。反向使用(从 Python 导入且没有图形界面)可能会变得复杂且令人沮丧。PyMOL 启动一个单独的线程来渲染图像,这些图像异步工作。

例如,这意味着你的代码可能与渲染器的位置不同。我已经在 GitHub 仓库中放了另一个名为 PyMol_Intro.py 的脚本;你会看到第二个 PNG 调用将在第一个还没有完成之前就开始。试试这个脚本代码,看看它应该如何运行,以及它实际是如何运行的。

从 GUI 角度来看,PyMOL 有很多优秀的文档,访问 www.pymolwiki.org/index.php/MovieSchool 可以获得。 如果你想制作电影,这是一个很好的起点,www.pymolwiki.org 是一个信息的宝库。

使用 Biopython 解析 mmCIF 文件

mmCIF 文件格式可能是未来的趋势。Biopython 目前还没有完全支持它的功能,但我们将看看当前有哪些功能。

正在准备

由于Bio.PDB不能自动下载 mmCIF 文件,你需要获取你的蛋白质文件并将其重命名为1tup.cif。它可以在github.com/PacktPublishing/Bioinformatics-with-Python-Cookbook-third-Edition/blob/master/Datasets.py中的1TUP.cif找到。

你可以在Chapter08/mmCIF.py笔记本文件中找到这些内容。

如何操作...

看一下以下步骤:

  1. 让我们解析文件。我们只需使用 MMCIF 解析器,而不是 PDB 解析器:

    from Bio import PDB
    parser = PDB.MMCIFParser()
    p53_1tup = parser.get_structure('P53', '1tup.cif')
    
  2. 让我们检查以下链条:

    def describe_model(name, pdb):
        print()
        for model in p53_1tup:
            for chain in model:
                print('%s - Chain: %s. Number of residues: %d. Number of atoms: %d.' %
                      (name, chain.id, len(chain), len(list(chain.get_atoms()))))
    describe_model('1TUP', p53_1tup)
    

输出将如下所示:

1TUP - Chain: E. Number of residues: 43\. Number of atoms: 442.
1TUP - Chain: F. Number of residues: 35\. Number of atoms: 449.
1TUP - Chain: A. Number of residues: 395\. Number of atoms: 1734.
1TUP - Chain: B. Number of residues: 265\. Number of atoms: 1593.
1TUP - Chain: C. Number of residues: 276\. Number of atoms: 1610.
  1. 许多字段在解析的结构中不可用,但可以通过使用更低级别的字典来检索这些字段,如下所示:

    mmcif_dict = PDB.MMCIF2Dict.MMCIF2Dict('1tup.cif')
    for k, v in mmcif_dict.items():
        print(k, v)
        print()
    

不幸的是,这个列表很大,需要一些后处理才能理解它,但它是可以获得的。

还有更多内容

你仍然可以获取来自 Biopython 提供的 mmCIF 文件的所有模型信息,因此解析器仍然非常有用。我们可以预期mmCIF解析器会有更多的开发,而不是PDB解析器。

这有一个 Python 库,由 PDB 的开发者提供,网址为mmcif.wwpdb.org/docs/sw-examples/python/html/index.xhtml

第十章:生物信息学管道

管道在任何数据科学环境中都至关重要。数据处理从来不是一项单一的任务。许多管道是通过临时脚本实现的。虽然这种方式可以有效使用,但在许多情况下,它们未能满足几个基本视角,主要是可重复性、可维护性和可扩展性。

在生物信息学中,您可以找到三种主要类型的管道系统:

  • 像 Galaxy (usegalaxy.org) 这样的框架,专为用户设计,即它们提供了易于使用的用户界面,并隐藏了大部分底层机制。

  • 编程工作流 — 针对代码接口的工作流,虽然是通用的,但来源于生物信息学领域。两个例子是 Snakemake (snakemake.readthedocs.io/) 和 Nextflow (www.nextflow.io/)。

  • 完全通用的工作流系统,例如 Apache Airflow (airflow.incubator.apache.org/),它采用一种不太以数据为中心的工作流管理方式。

在本章中,我们将讨论 Galaxy,尤其是对于那些支持不太倾向于编写代码解决方案的用户的生物信息学专家而言,Galaxy 尤为重要。虽然您可能不是这些管道系统的典型用户,但您仍然可能需要为它们提供支持。幸运的是,Galaxy 提供了 API,这是我们主要关注的内容。

我们还将讨论 Snakemake 和 Nextflow 作为通用工作流工具,这些工具有编程接口,最初来源于生物信息学领域。我们将涵盖这两种工具,因为它们是该领域中最常见的工具。我们将用 Snakemake 和 Nextflow 解决一个类似的生物信息学问题。我们将体验这两个框架,并希望能够决定哪个是我们的最爱。

这些配方的代码不是以笔记本的形式呈现,而是作为 Python 脚本,存放在本书仓库的Chapter09目录下。

本章中,您将找到以下配方:

  • 介绍 Galaxy 服务器

  • 使用 API 访问 Galaxy

  • 使用 Snakemake 开发变异分析管道

  • 使用 Nextflow 开发变异分析管道

介绍 Galaxy 服务器

Galaxy (galaxyproject.org/tutorials/g101/) 是一个开源系统,旨在帮助非计算用户进行计算生物学研究。它是目前最广泛使用且用户友好的管道系统。任何用户都可以在服务器上安装 Galaxy,但网络上也有许多公开访问的其他服务器,其中最著名的是 usegalaxy.org

接下来食谱的重点将是 Galaxy 的编程方面:使用 Galaxy API 进行接口连接,以及开发 Galaxy 工具以扩展其功能。在开始之前,强烈建议你先作为用户使用 Galaxy。你可以通过在usegalaxy.org创建一个免费账户并稍作体验来实现这一点。建议你了解工作流的基本知识。

准备就绪

在此食谱中,我们将使用 Docker 在本地安装 Galaxy 服务器。因此,需要一个本地 Docker 安装。不同操作系统的复杂度不同:在 Linux 上容易,在 macOS 上中等,在 Windows 上中到难。

此安装推荐用于接下来的两个食谱,但你也可以使用现有的公共服务器。请注意,公共服务器的接口可能会随时间变化,因此今天有效的内容明天可能会失效。如何使用公共服务器来执行接下来的两个食谱的说明,详见更多信息...部分。

如何操作…

看一下以下步骤。这些步骤假设你已经启用了 Docker 命令行:

  1. 首先,我们使用以下命令拉取 Galaxy Docker 镜像:

    docker pull bgruening/galaxy-stable:20.09
    

这将拉取 Björn Grüning 的精彩 Docker Galaxy 镜像。请使用20.09标签,如前面命令所示;任何更新的版本可能会破坏此食谱和下一个食谱。

  1. 在系统上创建一个目录。此目录将保存 Docker 容器在多次运行中的持久输出。

注意

Docker 容器对于磁盘空间是临时的。这意味着当你停止容器时,所有磁盘上的更改都会丢失。可以通过在 Docker 中挂载来自主机的卷来解决此问题,如下一个步骤所示。挂载卷中的所有内容将会持久化。

  1. 现在我们可以通过以下命令运行镜像:

    docker run -d -v YOUR_DIRECTORY:/export -p 8080:80 -p 8021:21 bgruening/galaxy-stable:20.09
    

YOUR_DIRECTORY替换为你在步骤 2中创建的目录的完整路径。如果前面的命令失败,请确保你有权限运行 Docker。不同操作系统的权限设置可能不同。

  1. 检查YOUR_DIRECTORY中的内容。第一次运行镜像时,它会创建所有需要的文件,以便在 Docker 运行之间持久化执行。这意味着会保持用户数据库、数据集和工作流。

将浏览器指向http://localhost:8080。如果遇到任何错误,稍等几秒钟。你应该能看到如下屏幕:

图 9.1 - Galaxy Docker 首页

图 9.1 - Galaxy Docker 首页

  1. 现在使用默认的用户名和密码组合:adminpassword,登录(参见顶部栏)。

  2. 从顶部菜单中选择用户,然后选择首选项

  3. 现在,选择管理 API 密钥

不要更改 API 密钥。前面的练习目的是让你知道 API 密钥的位置。在实际场景中,你需要进入这个页面获取你的密钥。请记下 API 密钥:fakekey。顺便说一句,正常情况下,这将是一个 MD5 哈希。

到目前为止,我们已经在服务器上安装了以下(默认)凭据:用户为admin,密码为password,API 密钥为fakekey。访问点是localhost:8080

还有更多内容

Björn Grüning 的镜像将在本章中使用的方式相当简单;毕竟,这不是一本关于系统管理或 DevOps 的书,而是一本关于编程的书。如果你访问github.com/bgruening/docker-galaxy-stable,你会看到有无数种配置镜像的方式,而且都有详细的文档。在这里,我们简单的方法足够满足我们的开发需求。

如果你不想在本地计算机上安装 Galaxy,可以使用公共服务器,例如usegalaxy.org来进行下一步操作。这个方法不是 100%万无一失的,因为服务会随时间变化,但它可能会非常接近。请按以下步骤进行:

  1. 在公共服务器上创建一个帐户(usegalaxy.org或其他服务器)。

  2. 请按照之前的说明获取你的 API 密钥。

  3. 在下一个步骤中,你需要替换主机、用户、密码和 API 密钥。

使用 API 访问 Galaxy

虽然 Galaxy 的主要用法是通过易于使用的 Web 界面,但它也提供了一个 REST API 供程序化访问。它提供了多种语言的接口,例如,Python 支持可以通过 BioBlend 获得(bioblend.readthedocs.io)。

在这里,我们将开发一个脚本,加载一个 BED 文件到 Galaxy,并调用一个工具将其转换为 GFF 格式。我们将通过 Galaxy 的 FTP 服务器加载文件。

准备工作

如果你没有完成前面的步骤,请阅读相应的更多内容...部分。代码已在前面步骤中准备的本地服务器上进行了测试,因此如果你在公共服务器上运行,可能需要进行一些调整。

我们的代码将需要在 Galaxy 服务器上进行身份验证,以执行必要的操作。由于安全性是一个重要问题,本教程在这方面不会完全天真。我们的脚本将通过 YAML 文件进行配置,例如:

rest_protocol: http
server: localhost
rest_port: 8080
sftp_port: 8022
user: admin
password: password
api_key: fakekey

我们的脚本不会接受这个文件作为纯文本,而是要求它被加密。也就是说,我们的安全计划中存在一个大漏洞:我们将使用 HTTP(而不是 HTTPS),这意味着密码将在网络上传输时以明文形式发送。显然,这不是一个好的解决方案,但考虑到空间的限制,我们只能做到这一点(特别是在前面的步骤中)。真正安全的解决方案需要使用 HTTPS。

我们将需要一个脚本,该脚本接受一个 YAML 文件并生成加密版本:

import base64
import getpass
from io import StringIO
import os
from ruamel.yaml import YAML
from cryptography.fernet import Fernet
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
password = getpass.getpass('Please enter the password:').encode()
salt = os.urandom(16)
kdf = PBKDF2HMAC(algorithm=hashes.SHA256(), length=32, salt=salt,
                 iterations=100000, backend=default_backend())
key = base64.urlsafe_b64encode(kdf.derive(password))
fernet = Fernet(key)
with open('salt', 'wb') as w:
    w.write(salt)
yaml = YAML()
content = yaml.load(open('galaxy.yaml', 'rt', encoding='utf-8'))
print(type(content), content)
output = StringIO()
yaml.dump(content, output)
print ('Encrypting:\n%s' % output.getvalue())
enc_output = fernet.encrypt(output.getvalue().encode())
with open('galaxy.yaml.enc', 'wb') as w:
    w.write(enc_output) 

前面的文件可以在 GitHub 仓库中的Chapter09/pipelines/galaxy/encrypt.py找到。

你将需要输入一个密码来进行加密。

前面的代码与 Galaxy 无关:它读取一个YAML文件,并使用用户提供的密码进行加密。它使用cryptography模块进行加密,并用ruaml.yaml处理YAML文件。输出两个文件:加密后的YAML文件和加密用的salt文件。出于安全考虑,salt文件不应公开。

这种保护凭证的方法远非复杂;它主要是为了说明在处理认证令牌时,你必须小心代码中硬编码的安全凭证。在网络上有很多硬编码安全凭证的实例。

如何操作…

请查看以下步骤,它们可以在Chapter09/pipelines/galaxy/api.py中找到:

  1. 我们从解密我们的配置文件开始。我们需要提供一个密码:

    import base64
    from collections import defaultdict
    import getpass
    import pprint
    import warnings
    from ruamel.yaml import YAML
    from cryptography.fernet import Fernet
    from cryptography.hazmat.backends import default_backend
    from cryptography.hazmat.primitives import hashes
    from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
    import pandas as pd
    Import paramiko
    from bioblend.galaxy import GalaxyInstance
    pp = pprint.PrettyPrinter()
    warnings.filterwarnings('ignore')
    # explain above, and warn
    with open('galaxy.yaml.enc', 'rb') as f:
        enc_conf = f.read()
    password = getpass.getpass('Please enter the password:').encode()
    with open('salt', 'rb') as f:
        salt = f.read()
    kdf = PBKDF2HMAC(algorithm=hashes.SHA256(), length=32, salt=salt,
                     iterations=100000, backend=default_backend())
    key = base64.urlsafe_b64encode(kdf.derive(password))
    fernet = Fernet(key)
    yaml = YAML()
    conf = yaml.load(fernet.decrypt(enc_conf).decode())
    

最后一行总结了所有内容:YAML模块将从解密后的文件中加载配置。请注意,我们还读取了salt,以便能够解密文件。

  1. 现在我们将获取所有配置变量,准备服务器 URL,并指定我们将要创建的 Galaxy 历史记录的名称(bioinf_example):

    server = conf['server']
    rest_protocol = conf['rest_protocol']
    rest_port = conf['rest_port']
    user = conf['user']
    password = conf['password']
    ftp_port = int(conf['ftp_port'])
    api_key = conf['api_key']
    rest_url = '%s://%s:%d' % (rest_protocol, server, rest_port)
    history_name = 'bioinf_example'
    
  2. 最后,我们能够连接到 Galaxy 服务器:

    gi = GalaxyInstance(url=rest_url, key=api_key)
    gi.verify = False
    
  3. 我们将列出所有可用的历史记录

    histories = gi.histories
    print('Existing histories:')
    for history in histories.get_histories():
        if history['name'] == history_name:
            histories.delete_history(history['id'])
        print('  - ' + history['name'])
    print()
    

在第一次执行时,你将获得一个未命名的历史记录,但在之后的执行中,你还将获得bioinf_example,我们将在此阶段删除它,以便从干净的状态开始。

  1. 之后,我们创建了bioinf_example历史记录:

    ds_history = histories.create_history(history_name)
    

如果你愿意,你可以在 Web 界面上检查,你会在那里看到新的历史记录。

  1. 现在我们将上传文件;这需要一个 SFTP 连接。文件通过以下代码提供:

    print('Uploading file')
    transport = paramiko.Transport((server, sftp_port))
    transport.connect(None, user, password)
    sftp = paramiko.SFTPClient.from_transport(transport)
    sftp.put('LCT.bed', 'LCT.bed')
    sftp.close()
    transport.close()
    
  2. 现在我们将告诉 Galaxy 将 FTP 服务器上的文件加载到其内部数据库中:

    gi.tools.upload_from_ftp('LCT.bed', ds_history['id'])
    
  3. 让我们总结一下我们历史记录的内容:

    def summarize_contents(contents):
     summary = defaultdict(list)
     for item in contents:
     summary['íd'].append(item['id'])
     summary['híd'].append(item['hid'])
     summary['name'].append(item['name'])
     summary['type'].append(item['type'])
     summary['extension'].append(item['extension'])
     return pd.DataFrame.from_dict(summary)
    print('History contents:')
    pd_contents = summarize_contents(contents)
    print(pd_contents)
    print()
    

我们只有一个条目:

                 íd  híd     name  type extension
0  f2db41e1fa331b3e    1  LCT.bed  file      auto
  1. 让我们检查一下BED文件的元数据:

    print('Metadata for LCT.bed')
    bed_ds = contents[0]
    pp.pprint(bed_ds)
    print()
    

结果包括以下内容:

{'create_time': '2018-11-28T21:27:28.952118',
 'dataset_id': 'f2db41e1fa331b3e',
 'deleted': False,
 'extension': 'auto',
 'hid': 1,
 'history_content_type': 'dataset',
 'history_id': 'f2db41e1fa331b3e',
 'id': 'f2db41e1fa331b3e',
 'name': 'LCT.bed',
 'purged': False,
 'state': 'queued',
 'tags': [],
 'type': 'file',
 'type_id': 'dataset-f2db41e1fa331b3e',
 'update_time': '2018-11-28T21:27:29.149933',
 'url': '/api/histories/f2db41e1fa331b3e/contents/f2db41e1fa331b3e',
 'visible': True}
  1. 让我们将注意力转向服务器上现有的工具,并获取有关它们的元数据:

    print('Metadata about all tools')
    all_tools = gi.tools.get_tools()
    pp.pprint(all_tools)
    print()
    

这将打印出一个长长的工具列表。

  1. 现在让我们获取一些关于我们工具的信息:

    bed2gff = gi.tools.get_tools(name='Convert BED to GFF')[0]
    print("Converter metadata:")
    pp.pprint(gi.tools.show_tool(bed2gff['id'], io_details=True, link_details=True))
    print()
    

工具的名称在前面的步骤中可用。请注意,我们获取的是列表中的第一个元素,因为理论上可能安装了多个版本的工具。简化后的输出如下:

{'config_file': '/galaxy-central/lib/galaxy/datatypes/converters/bed_to_gff_converter.xml',
 'id': 'CONVERTER_bed_to_gff_0',
 'inputs': [{'argument': None,
             'edam': {'edam_data': ['data_3002'],
                      'edam_formats': ['format_3003']},
             'extensions': ['bed'],
             'label': 'Choose BED file',
             'multiple': False,
             'name': 'input1',
             'optional': False,
             'type': 'data',
             'value': None}],
 'labels': [],
 'link': '/tool_runner?tool_id=CONVERTER_bed_to_gff_0',
 'min_width': -1,
 'model_class': 'Tool',
 'name': 'Convert BED to GFF',
 'outputs': [{'edam_data': 'data_1255',
              'edam_format': 'format_2305',
              'format': 'gff',
              'hidden': False,
              'model_class': 'ToolOutput',
              'name': 'output1'}],
 'panel_section_id': None,
 'panel_section_name': None,
 'target': 'galaxy_main',
 'version': '2.0.0'}
  1. 最后,让我们运行一个工具,将我们的BED文件转换为GFF

    def dataset_to_param(dataset):
        return dict(src='hda', id=dataset['id'])
    tool_inputs = {
        'input1': dataset_to_param(bed_ds)
        }
    gi.tools.run_tool(ds_history['id'], bed2gff['id'], tool_inputs=tool_inputs)
    

可以在前面的步骤中检查工具的参数。如果你进入 Web 界面,你会看到类似以下的内容:

图 9.2 - 通过 Galaxy 的 Web 界面检查我们脚本的结果

图 9.2 - 通过 Galaxy 的 Web 界面检查我们脚本的结果

因此,我们通过其 REST API 访问了 Galaxy。

使用 Snakemake 部署变异分析管道

Galaxy 主要面向那些不太倾向于编程的用户。即使你更喜欢编程友好的环境,了解如何使用 Galaxy 仍然很重要,因为它的广泛应用。幸运的是,Galaxy 提供了一个 API 可以进行交互。不过,如果你需要一个更适合编程的流程,也有很多替代方案。在本章中,我们将探讨两个广泛使用的编程友好型流程:snakemake 和 Nextflow。在本配方中,我们考虑使用 snakemake

Snakemake 是用 Python 实现的,并且与 Python 共享许多特性。话虽如此,它的基本灵感来源于 Makefile,这是久负盛名的 make 构建系统所使用的框架。

在这里,我们将使用 snakemake 开发一个迷你变异分析流程。这里的目标不是做出正确的科学部分—我们在其他章节中会讨论这个—而是展示如何使用 snakemake 创建流程。我们的迷你流程将下载 HapMap 数据,对其进行 1% 的子采样,做一个简单的 PCA,并绘制图形。

准备工作

你需要在安装了 snakemake 的同时安装 Plink 2。为了展示执行策略,你还需要 Graphviz 来绘制执行过程。我们将定义以下任务:

  1. 下载数据

  2. 解压它

  3. 对其进行 1% 的子采样

  4. 在 1% 子样本上计算 PCA

  5. 绘制 PCA 图

我们的流程配方将包含两个部分:用 snakemake 编写的实际流程和一个 Python 支持脚本。

这段 snakemake 代码可以在 Chapter09/snakemake/Snakefile 中找到,而 Python 支持脚本则在 Chapter09/snakemake/plot_pca.py 中。

如何执行……

  1. 第一个任务是下载数据:

    from snakemake.remote.HTTP import RemoteProvider as HTTPRemoteProvider 
    HTTP = HTTPRemoteProvider()
    download_root = "https://ftp.ncbi.nlm.nih.gov/hapmap/genotypes/hapmap3_r3"
    remote_hapmap_map = f"{download_root}/plink_format/hapmap3_r3_b36_fwd.consensus.qc.poly.map.gz"
    remote_hapmap_ped = f"{download_root}/plink_format/hapmap3_r3_b36_fwd.consensus.qc.poly.ped.gz"
    remote_hapmap_rel = f"{download_root}/relationships_w_pops_041510.txt"
    
    rule plink_download:
        input:
            map=HTTP.remote(remote_hapmap_map, keep_local=True),
            ped=HTTP.remote(remote_hapmap_ped, keep_local=True),
            rel=HTTP.remote(remote_hapmap_rel, keep_local=True)
    
        output:
            map="scratch/hapmap.map.gz",
            ped="scratch/hapmap.ped.gz",
            rel="data/relationships.txt"
    
        shell:
            "mv {input.map} {output.map};"
            "mv {input.ped} {output.ped};"
            "mv {input.rel} {output.rel}"
    

Snakemake 的语言依赖于 Python,正如你从最前面的几行代码可以看到的那样,这些代码从 Python 的角度来看应该非常容易理解。核心部分是规则。它有一组输入流,在我们的案例中通过 HTTP.remote 进行渲染,因为我们处理的是远程文件,接着是输出。我们将两个文件放在 scratch 目录(那些尚未解压的文件)中,另一个文件放在 data 目录。最后,我们的流程代码是一个简单的 shell 脚本,它将下载的 HTTP 文件移到最终位置。注意 shell 脚本如何引用输入和输出。

  1. 使用这个脚本,下载文件变得非常简单。只需在命令行中运行以下命令:

    snakemake -c1 data/relationships.txt
    

这告诉 snakemake 你希望生成 data/relationships.txt 文件。我们将使用单核 -c1。由于这是 plink_download 规则的输出,规则将会被执行(除非该文件已经存在—如果是这样,snakemake 就什么都不做)。以下是简化版的输出:

Building DAG of jobs...
Using shell: /usr/bin/bash
Provided cores: 1 (use --cores to define parallelism)
Rules claiming more threads will be scaled down.
Job stats:
job               count    min threads    max threads
--------------  -------  -------------  -------------
plink_download        1              1              1
total                 1              1              1

Select jobs to execute...

[Mon Jun 13 18:54:26 2022]
rule plink_download:
    input: ftp.ncbi.nlm.nih.gov/hapmap/ge [...]
    output: [..], data/relationships.txt
    jobid: 0
    reason: Missing output files: data/relationships.txt
    resources: tmpdir=/tmp

Downloading from remote: [...]relationships_w_pops_041510.txt
Finished download.
[...]
Finished job 0.
1 of 1 steps (100%) done

Snakemake 会给你一些关于将要执行的任务的信息,并开始运行这些任务。

  1. 现在我们有了数据,接下来看看解压文件的规则:

    PLINKEXTS = ['ped', 'map']
    rule uncompress_plink:
        input:
            "scratch/hapmap.{plinkext}.gz"
        output:
            "data/hapmap.{plinkext}"
        shell:
            "gzip -dc {input} > {output}"
    

这里最有趣的特点是我们可以指定多个文件进行下载。注意PLINKEXTS列表是如何在代码中转换成离散的plinkext元素的。你可以通过请求规则的输出执行它。

  1. 现在,让我们将数据下采样到 1%:

    rule subsample_1p:
        input:
            "data/hapmap.ped",
            "data/hapmap.map"
    
        output:
            "data/hapmap1.ped",
            "data/hapmap1.map"
    
        run:
            shell(f"plink2 --pedmap {input[0][:-4]} --out {output[0][:-4]} --thin 0.01 --geno 0.1 --export ped")
    

新的内容在最后两行:我们没有使用script,而是使用run。这告诉snakemake执行是基于 Python 的,并且有一些额外的功能可用。在这里,我们看到的是 shell 函数,它执行一个 shell 脚本。这个字符串是一个 Python 的f-字符串——注意字符串中对snakemakeinputoutput变量的引用。你可以在这里放入更复杂的 Python 代码——例如,你可以遍历输入数据。

提示

在这里,我们假设 Plink 是可用的,因为我们已经预安装了它,但snakemake确实提供了一些功能来处理依赖关系。更具体地说,snakemake规则可以通过一个指向conda依赖的YAML文件进行注释。

  1. 现在我们已经对数据进行了下采样,让我们计算 PCA。在这种情况下,我们将使用 Plink 的内部 PCA 框架来进行计算:

    rule plink_pca:
        input:
            "data/hapmap1.ped",
            "data/hapmap1.map"
        output:
            "data/hapmap1.eigenvec",
            "data/hapmap1.eigenval"
        shell:
            "plink2 --pca --file data/hapmap1 -out data/hapmap1"
    
  2. 和大多数管道系统一样,snakemake构建了一个snakemake来为你展示执行的 DAG,以生成你的请求。例如,为了生成 PCA,使用以下命令:

    snakemake --dag data/hapmap1.eigenvec | dot -Tsvg > bio.svg
    

这将生成以下图像:

图 9.3 - 计算 PCA 的 DAG

图 9.3 - 计算 PCA 的 DAG

  1. 最后,让我们为 PCA 生成plot规则:

    rule plot_pca:
        input:
            "data/hapmap1.eigenvec",
            "data/hapmap1.eigenval"
    
        output:
            "pca.png"
    
        script:
            "./plot_pca.py"
    

plot规则引入了一种新的执行类型,script。在这种情况下,调用了一个外部的 Python 脚本来处理规则。

  1. 我们用来生成图表的 Python 脚本如下:

    import pandas as pd
    
    eigen_fname = snakemake.input[0] if snakemake.input[0].endswith('eigenvec') else snakemake.input[1]
    pca_df = pd.read_csv(eigen_fname, sep='\t') 
    ax = pca_df.plot.scatter(x=2, y=3, figsize=(16, 9))
    ax.figure.savefig(snakemake.output[0])
    

Python 脚本可以访问snakemake对象。这个对象暴露了规则的内容:注意我们如何使用input来获取 PCA 数据,使用output来生成图像。

  1. 最后,生成粗略图表的代码如下:

图 9.4 - Snakemake 管道生成的非常粗略的 PCA

图 9.4 - Snakemake 管道生成的非常粗略的 PCA

还有更多

上面的食谱是为了在简单配置的snakemake上运行制作的。snakemake中还有许多其他构建规则的方式。

我们没有讨论的最重要问题是,snakemake可以在多种不同环境中执行代码,从本地计算机(如我们的案例)、本地集群,到云端。要求除了使用本地计算机运行snakemake外更多的功能是不合理的,但不要忘了snakemake可以管理复杂的计算环境。

记住,虽然snakemake是用 Python 实现的,但其概念上基于make。是否喜欢这种(蛇形)设计是一个主观的分析。如果想了解一种替代的设计方法,查看下一个食谱,它使用 Nextflow。

使用 Nextflow 部署变异分析管道

在生物信息学的管道框架领域,有两个主要的参与者:snakemake和 Nextflow。它们提供管道功能,但设计方法不同。Snakemake 基于 Python,但它的语言和理念来源于用于编译有依赖关系的复杂程序的make工具。Nextflow 是基于 Java 的(更准确地说,它是用 Groovy 实现的——一种运行在 Java 虚拟机上的语言),并且有自己的snakemake,可以选择适合你需求的那个。

提示

评估管道系统有许多视角。这里,我们呈现了一个基于用来指定管道的语言的视角。然而,选择管道系统时,你还应该考虑其他方面。例如,它是否能很好地支持你的执行环境(如本地集群或云环境),是否支持你的工具(或者是否允许轻松开发扩展以应对新工具),以及它是否提供良好的恢复和监控功能?

在这里,我们将开发一个 Nextflow 管道,提供与我们使用snakemake实现的相同功能,从而允许从管道设计角度进行公平比较。这里的目标不是搞清楚科学部分——我们将在其他章节中讨论这个——而是展示如何使用snakemake创建管道。我们的迷你管道将下载 HapMap 数据,进行 1%的子抽样,做一个简单的 PCA,并绘制出来。

做好准备

你需要安装 Plink 2 以及 Nextflow。Nextflow 本身需要一些来自 Java 领域的软件:特别是 Java 运行时环境和 Groovy。

我们将定义以下任务:

  1. 下载数据

  2. 解压它

  3. 对其进行 1%的子抽样

  4. 对 1%子抽样数据进行 PCA 计算

  5. 绘制 PCA 图

相关的 Nextflow 代码可以在Chapter09/nextflow/pipeline.nf找到。

如何做到这一点…

  1. 第一个任务是下载数据:

    nextflow.enable.dsl=2
    download_root = "https://ftp.ncbi.nlm.nih.gov/hapmap/genotypes/hapmap3_r3"
     process plink_download {
      output:
      path 'hapmap.map.gz'
      path 'hapmap.ped.gz'
      script:
      """
      wget $download_root/plink_format/hapmap3_r3_b36_fwd.consensus.qc.poly.map.gz -O hapmap.map.gz
      wget $download_root/plink_format/hapmap3_r3_b36_fwd.consensus.qc.poly.ped.gz -O hapmap.ped.gz
       """
    }
    

请记住,管道的基础语言不是 Python,而是 Groovy,因此语法会有些不同,比如使用大括号表示代码块或忽略缩进。

我们创建了一个名为plink_download的过程(Nextflow 中的管道构建块),用于下载 Plink 文件。它只指定了输出。第一个输出将是hapmap.map.gz文件,第二个输出将是hapmap.ped.gz。该过程将有两个输出通道(Nextflow 中的另一个概念,类似于流),可以被另一个过程消费。

该过程的代码默认是一个 bash 脚本。值得注意的是,脚本输出的文件名与输出部分是同步的。同时,也要注意我们是如何引用管道中定义的变量的(例如我们的例子中的download_root)。

  1. 现在我们来定义一个过程,使用 HapMap 文件并解压它们:

    process uncompress_plink {
      publishDir 'data', glob: '*', mode: 'copy'
    
      input:
      path mapgz
      path pedgz
    
      output:
      path 'hapmap.map'
      path 'hapmap.ped'
    
      script:
      """
      gzip -dc $mapgz > hapmap.map
      gzip -dc $pedgz > hapmap.ped
      """
    }
    

在这个过程中有三个需要注意的问题:我们现在有了一些输入(记住我们从上一个过程中得到了一些输出)。我们的脚本现在也引用了输入变量($mapgz$pedgz)。最后,我们通过使用publishDir发布输出。因此,任何未发布的文件将只会被暂时存储。

  1. 让我们指定一个下载并解压文件的工作流的第一个版本:

    workflow {
        plink_download | uncompress_plink
    }
    
  2. 我们可以通过在命令行中运行以下命令来执行工作流:

    nextflow run pipeline.nf -resume
    

最后的resume标志将确保管道从已完成的步骤继续执行。步骤会在本地执行时存储在work目录中。

  1. 如果我们删除work目录,我们不希望下载 HapMap 文件,如果它们已经被发布。由于这些文件不在work目录中,因此不直接跟踪,我们需要修改工作流,以便在已发布的目录中跟踪数据:

    workflow {
        ped_file = file('data/hapmap.ped')
        map_file = file('data/hapmap.map')
        if (!ped_file.exists() | !map_file.exists()) {
            plink_download | uncompress_plink
        }
    }
    

还有其他方法可以做到这一点,但我想介绍一些 Groovy 代码,因为你可能有时需要在 Groovy 中编写代码。正如你很快会看到的,也有方法可以使用 Python 代码。

  1. 现在,我们需要对数据进行亚采样:

    process subsample_1p {
      input:
      path 'hapmap.map'
      path 'hapmap.ped'
    
      output:
      path 'hapmap1.map'
      path 'hapmap1.ped'
    
      script:
      """
      plink2 --pedmap hapmap --out hapmap1 --thin 0.01 --geno 0.1 --export ped
      """
    }
    
  2. 现在,让我们使用 Plink 计算 PCA:

    process plink_pca {
      input:
      path 'hapmap.map'
      path 'hapmap.ped'
      output:
      path 'hapmap.eigenvec'
      path 'hapmap.eigenval'
       script:
      """
      plink2 --pca --pedmap hapmap -out hapmap
      """
    }
    
  3. 最后,让我们绘制 PCA 图:

    process plot_pca {
      publishDir '.', glob: '*', mode: 'copy'
    
      input:
      path 'hapmap.eigenvec'
      path 'hapmap.eigenval'
    
      output:
      path 'pca.png'
    
      script:
      """
      #!/usr/bin/env python
      import pandas as pd
    
      pca_df = pd.read_csv('hapmap.eigenvec', sep='\t') 
      ax = pca_df.plot.scatter(x=2, y=3, figsize=(16, 9))
      ax.figure.savefig('pca.png')
      """
    }
    

该代码的新特性是我们使用 shebang(#!)操作符指定了 bash 脚本,这使我们能够调用外部脚本语言来处理数据。

这是我们的最终工作流:

workflow {
    ped_file = file('data/hapmap.ped')
    map_file = file('data/hapmap.map')
    if (!ped_file.exists() | !map_file.exists()) {
        plink_download | uncompress_plink | subsample_1p | plink_pca | plot_pca
    }
    else {
        subsample_1p(
            Channel.fromPath('data/hapmap.map'),
            Channel.fromPath('data/hapmap.ped')) | plink_pca | plot_pca
    }
}

我们要么下载数据,要么使用已经下载的数据。

虽然有其他方言用于设计完整的工作流,但我想让你注意我们如何在文件可用时使用subsample_1p;我们可以显式地将两个通道传递给一个过程。

  1. 我们可以运行管道并请求执行 HTML 报告:

    nextflow run pipeline.nf -with-report report/report.xhtml
    

报告非常详细,能够帮助你从不同角度了解管道中哪些部分是开销较大的,无论是与时间、内存、CPU 消耗还是 I/O 相关。

还有更多内容

这是一个简单的 Nextflow 入门示例,希望能让你对这个框架有所了解,尤其是让你能够与snakemake进行比较。Nextflow 有更多的功能,建议你查看它的网站。

snakemake一样,我们没有讨论的最重要问题是 Nextflow 可以在许多不同的环境中执行代码,从本地计算机、现场集群到云。请查阅 Nextflow 的文档,了解当前支持哪些计算环境。

无论底层语言多么重要,Groovy 与 Nextflow,Python 与snakemake一样,都必须比较其他因素。这不仅包括两个管道可以在哪些地方执行(本地、集群或云),还包括框架的设计,因为它们使用的是截然不同的范式。

第十一章:生物信息学中的机器学习

机器学习在许多不同的领域中都有应用,计算生物学也不例外。机器学习在该领域有着无数的应用,最古老且最为人熟知的应用之一就是使用主成分分析PCA)通过基因组学研究种群结构。随着该领域的蓬勃发展,还有许多其他潜在的应用。在本章中,我们将从生物信息学的角度介绍机器学习的概念。

鉴于机器学习是一个非常复杂的主题,足以填满一本书,在这里我们打算采用直观的方法,让你大致了解一些机器学习技术如何帮助解决生物学问题。如果你发现这些技术有用,你将理解基本概念,并可以进一步阅读更详细的文献。

如果你使用的是 Docker,由于本章中的所有库对于数据分析都是基础库,它们都可以在 Docker 镜像tiagoantao/bioinformatics_ml中找到。

在本章中,我们将覆盖以下配方:

  • 用 PCA 示例介绍 scikit-learn

  • 使用 PCA 聚类来分类样本

  • 使用决策树探索乳腺癌特征

  • 使用随机森林预测乳腺癌结果

用 PCA 示例介绍 scikit-learn

PCA 是一种统计方法,用于将多个变量的维度降低到一个较小的、线性无关的子集。在第六章中,我们已经看过基于使用外部应用程序的 PCA 实现。在这个配方中,我们将为种群遗传学实现相同的 PCA,但将使用scikit-learn库。Scikit-learn 是 Python 中用于机器学习的基础库之一,本配方是对该库的介绍。PCA 是一种无监督的机器学习方法——我们不提供样本的类别信息。我们将在本章的其他配方中讨论有监督的技术。

提醒一下,我们将计算来自 HapMap 项目的 11 个人类种群的 PCA。

准备工作

你需要运行第六章中的第一个配方,以生成hapmap10_auto_noofs_ld_12 PLINK 文件(等位基因记录为 1 和 2)。从种群遗传学的角度来看,我们需要 LD 修剪标记来生成可靠的 PCA。我们这里不使用后代样本,因为它可能会导致结果偏差。我们的配方将需要pygenomics库,可以使用以下命令进行安装:

pip install pygenomics

代码位于Chapter10/PCA.py笔记本中。

如何实现...

看一下以下步骤:

  1. 我们首先加载样本的元数据。在我们的案例中,我们将加载每个样本所属的人类种群:

    import os
    from sklearn.decomposition import PCA
    import numpy as np
    from genomics.popgen.pca import plot
    f = open('../Chapter06/relationships_w_pops_041510.txt')
    ind_pop = {}
    f.readline()  # header
    for l in f:
        toks = l.rstrip().split('\t')
        fam_id = toks[0]
        ind_id = toks[1]
        pop = toks[-1]
        ind_pop['/'.join([fam_id, ind_id])] = pop
    f.close()
    
  2. 我们现在获得了个体的顺序以及我们将要处理的 SNP 数量:

    f = open('../Chapter06/hapmap10_auto_noofs_ld_12.ped')
    ninds = 0
    ind_order = []
    for line in f:
        ninds += 1
        toks = line[:100].replace(' ', '\t').split('\t')
        fam_id = toks[0]
        ind_id = toks[1]
        ind_order.append('%s/%s' % (fam_id, ind_id))
    nsnps = (len(line.replace(' ', '\t').split('\t')) - 6) // 2
    f.close()
    
  3. 我们创建将要输入 PCA 的数组:

    pca_array = np.empty((ninds, nsnps), dtype=int)
    print(pca_array.shape)
    f = open('../Chapter06/hapmap10_auto_noofs_ld_12.ped')
    for ind, line in enumerate(f):
        snps = line.replace(' ', '\t').split('\t')[6:]
        for pos in range(len(snps) // 2):
            a1 = int(snps[2 * pos])
            a2 = int(snps[2 * pos])
            my_code = a1 + a2 - 2
            pca_array[ind, pos] = my_code
    f.close()
    
  4. 最后,我们计算包含最多八个成分的 PCA。然后,使用transform方法获取所有样本的 8 维坐标。

    my_pca = PCA(n_components=8)
    my_pca.fit(pca_array)
    trans = my_pca.transform(pca_array)
    
  5. 最后,我们绘制 PCA 图:

    sc_ind_comp = {}
    for i, ind_pca in enumerate(trans):
        sc_ind_comp[ind_order[i]] = ind_pca
    plot.render_pca_eight(sc_ind_comp, cluster=ind_pop)
    

图 10.1 - 由 scikit-learn 生成的我们数据集的 PC1 到 PC8

图 10.1 - 由 scikit-learn 生成的我们数据集的 PC1 到 PC8

还有更多...

对于科学期刊中的发布,我建议使用第六章中的食谱,因为它基于一个已发布的、广受好评的方法。也就是说,这段代码的结果在定性上是相似的,并且以非常类似的方式对数据进行了聚类(如果你与第六章中的图进行比较,垂直轴方向的反转对于解读 PCA 图表而言是无关紧要的)。

使用 PCA 进行聚类来对样本进行分类

基因组学中的 PCA 可以让我们看到样本如何聚类。在许多情况下,同一群体的个体会聚集在图表的同一区域。但我们希望进一步预测新个体在群体中的位置。为此,我们将从 PCA 数据开始,因为它进行了降维处理—使得处理数据更为简便—然后应用 K-Means 聚类算法来预测新样本的位置。我们将使用与上述食谱相同的数据集。我们将使用除了一个样本外的所有样本来训练算法,然后预测剩下的样本位置。

K-Means 聚类可以是监督算法的一个例子。在这类算法中,我们需要一个训练数据集,以便算法能够学习。训练算法之后,它将能够对新样本预测某个结果。在我们的案例中,我们希望能够预测群体。

警告

当前的食谱旨在作为对监督算法及其背后概念的温和介绍。我们训练算法的方式远非最佳。关于如何正确训练一个监督算法的问题将在本章最后一个食谱中提到。

准备工作

我们将使用与之前食谱中相同的数据。此食谱的代码可以在Chapter10/Clustering.py中找到。

如何操作...

让我们来看一下:

  1. 我们首先加载群体信息——这与我们在之前的食谱中所做的相似:

    import os
    import matplotlib.pyplot as plt
    from sklearn.cluster import KMeans
    from sklearn.decomposition import PCA
    import numpy as np
    from genomics.popgen.pca import plot
    f = open('../Chapter06/relationships_w_pops_041510.txt')
    ind_pop = {}
    f.readline()  # header
    for l in f:
        toks = l.rstrip().split('\t')
        fam_id = toks[0]
        ind_id = toks[1]
        pop = toks[-1]
        ind_pop['/'.join([fam_id, ind_id])] = pop
    f.close()
    
    f = open('../Chapter06/hapmap10_auto_noofs_ld_12.ped')
    ninds = 0
    ind_order = []
    for line in f:
        ninds += 1
        toks = line[:100].replace(' ', '\t').split('\t') #  for speed
        fam_id = toks[0]
        ind_id = toks[1]
        ind_order.append('%s/%s' % (fam_id, ind_id))
    nsnps = (len(line.replace(' ', '\t').split('\t')) - 6) // 2
    print (nsnps)
    f.close()
    
  2. 现在,我们将所有样本数据(SNPs)加载到一个 NumPy 数组中:

    all_array = np.empty((ninds, nsnps), dtype=int)
    f = open('../Chapter06/hapmap10_auto_noofs_ld_12.ped')
    for ind, line in enumerate(f):
        snps = line.replace(' ', '\t').split('\t')[6:]
        for pos in range(len(snps) // 2):
            a1 = int(snps[2 * pos])
            a2 = int(snps[2 * pos])
            my_code = a1 + a2 - 2
            all_array[ind, pos] = my_code
    f.close()
    
  3. 我们将数组分成两个数据集,即包含所有个体(除一个外)作为训练集,和单个个体用于测试的案例:

    predict_case = all_array[-1, :]
    pca_array = all_array[:-1,:]
    
    last_ind = ind_order[-1]
    last_ind, ind_pop[last_ind]
    

我们的测试案例是个体 Y076/NA19124,我们知道他属于约鲁巴族群体。

  1. 现在,我们计算用于 K-Means 聚类的训练集的 PCA:

    my_pca = PCA(n_components=2)
    my_pca.fit(pca_array)
    trans = my_pca.transform(pca_array)
    
    sc_ind_comp = {}
    for i, ind_pca in enumerate(trans):
        sc_ind_comp[ind_order[i]] = ind_pca
    plot.render_pca(sc_ind_comp, cluster=ind_pop)
    

这是输出结果,将有助于检查聚类结果:

图 10.2 - 具有群体色彩编码的 PC1 和 PC2

图 10.2 - PC1 和 PC2 与按种群编码的颜色

  1. 在我们开始计算 K 均值聚类之前,先写一个函数来绘制运行算法后的聚类面:

    def plot_kmeans_pca(trans, kmeans):
        x_min, x_max = trans[:, 0].min() - 1, trans[:, 0].max() + 1
        y_min, y_max = trans[:, 1].min() - 1, trans[:, 1].max() + 1
        mesh_x, mesh_y = np.meshgrid(np.arange(x_min, x_max, 0.5), np.arange(y_min, y_max, 0.5))
    
        k_surface = kmeans.predict(np.c_[mesh_x.ravel(), mesh_y.ravel()]).reshape(mesh_x.shape)
        fig, ax = plt.subplots(1,1, dpi=300)
        ax.imshow(
            k_surface, origin="lower", cmap=plt.cm.Pastel1,
            extent=(mesh_x.min(), mesh_x.max(), mesh_y.min(), mesh_y.max()),
        )
        ax.plot(trans[:, 0], trans[:, 1], "k.", markersize=2)
        ax.set_title("KMeans clustering of PCA data")
        ax.set_xlim(x_min, x_max)
        ax.set_ylim(y_min, y_max)
        ax.set_xticks(())
        ax.set_yticks(())
        return ax
    
  2. 现在让我们用我们的样本来拟合算法。因为我们有 11 个人群,我们将训练 11 个簇:

    kmeans11 = KMeans(n_clusters=11).fit(trans)
    plot_kmeans_pca(trans, kmeans11)
    

这里是输出结果:

图 10.3 - 11 个簇的簇面

图 10.3 - 11 个簇的簇面

如果你与这里的图进行比较,你会直观地看到这个聚类没有多大意义:它并没有很好地映射到已知的人群上。有人可能会认为,使用 11 个簇的聚类算法并不是很有用。

提示

scikit-learn 中实现了许多其他聚类算法,在多种情况下,它们的表现可能优于 K 均值聚类。你可以在scikit-learn.org/stable/modules/clustering.xhtml找到它们。值得怀疑的是,在这个特定的案例中,任何替代方法都不太可能在 11 个簇的情况下表现得更好。

  1. 尽管看起来 K 均值聚类无法解决 11 个人群的划分,但如果我们使用不同数量的簇,或许它仍然可以提供一些预测。仅通过查看图表,我们可以看到四个独立的块。如果我们使用四个簇,会得到什么结果呢?

    kmeans4 = KMeans(n_clusters=4).fit(trans)
    plot_kmeans_pca(trans, kmeans4)
    

这里是输出结果:

图 10.4 - 四个簇的簇面

图 10.4 - 四个簇的簇面

四个群体现在大致清晰了。但它们直观上有意义吗?如果有,那么我们可以利用这种聚类方法。事实上,它们确实有意义。左侧的簇由非洲人口组成,顶部的簇由欧洲人组成,底部的簇由东亚人组成。中间的簇最为难以理解,因为它包含了古吉拉特人和墨西哥后裔,但这种混合最初来自于主成分分析(PCA),而非聚类本身所致。

  1. 让我们看看我们漏掉的那个单独个案的预测结果:

    pca_predict = my_pca.transform([predict_case])
    kmeans4.predict(pca_predict)
    

我们的样本被预测为属于簇 1。现在我们需要进一步挖掘一下。

  1. 让我们来看看簇 1 代表什么。我们取训练集中的最后一个个体,他也是一名约鲁巴人,看看他被分配到了哪个簇:

    last_train = ind_order[-2]
    last_train, ind_pop[last_train]
    kmeans4.predict(trans)[0]
    

确实是簇 1,因此预测是正确的。

还有更多...

值得重申的是,我们正在努力实现对机器学习的直观理解。在这个阶段,你应该对监督学习能够带来什么有所了解,并且已经掌握了一个聚类算法的使用示例。关于训练机器学习算法的过程,还有很多内容值得探讨,我们将在最后的食谱中部分揭示。

使用决策树探索乳腺癌特征

当我们接收到一个数据集时,第一个问题之一就是决定从哪里开始分析。一开始,往往会有一种迷茫的感觉,不知道该先做什么。这里,我们将展示基于决策树的探索性方法。决策树的最大优点是,它们会给我们提供构建决策树的规则,让我们初步了解数据的情况。

在这个示例中,我们将使用一个包含乳腺癌患者特征观察的数据集。该数据集包含 699 条数据,包含如肿块厚度、细胞大小的均匀性或染色质类型等信息。结果是良性或恶性肿瘤。特征值被编码为 0 到 10 之间的值。关于该项目的更多信息可以在archive.ics.uci.edu/ml/datasets/breast+cancer+wisconsin+%28diagnostic%29中找到。

准备工作

我们将下载数据及其文档:

wget http://archive.ics.uci.edu/ml/machine-learning-databases/breast-cancer-wisconsin/breast-cancer-wisconsin.data
wget http://archive.ics.uci.edu/ml/machine-learning-databases/breast-cancer-wisconsin/breast-cancer-wisconsin.names

数据文件的格式为 CSV 文件。关于内容的信息可以在第二个下载的文件中找到。

这个代码可以在Chapter10/Decision_Tree.py中找到。

如何操作...

按照以下步骤操作:

  1. 我们做的第一件事是移除一小部分数据不完整的个体:

    import numpy as np
    import matplotlib.pyplot as plt
    import pandas as pd
    from sklearn import tree
    f = open('breast-cancer-wisconsin.data')
    w = open('clean.data', 'w')
    for line in f:
        if line.find('?') > -1:
            continue
        w.write(line)
    f.close()
    w.close()
    

提示

移除数据不完整的个体在这种情况下是合适的,因为它们只是数据集的一小部分,而且我们只是进行探索性分析。如果数据缺失很多,或者我们需要做更严格的分析,你需要使用处理缺失数据的方法,但我们这里不会进行探讨。

  1. 现在我们将读取数据,为所有列命名:

    column_names = [
        'sample_id', 'clump_thickness', 'uniformity_cell_size',
        'uniformity_cell shape', 'marginal_adhesion',
        'single_epithelial_cell_size', 'bare_nuclei',
        'bland_chromatin', 'normal_nucleoli', 'mitoses',
        'class'
    ]
    samples = pd.read_csv('clean.data', header=None, names=column_names, index_col=0)
    
  2. 现在我们将特征与结果分离,并使用 0 和 1 重新编码结果:

    training_input = samples.iloc[:,:-1]
    target = samples.iloc[:,-1].apply(lambda x: 0 if x == 2 else 1)
    
  3. 现在让我们基于这些数据创建一个最大深度为 3 的决策树:

    clf = tree.DecisionTreeClassifier(max_depth=3)
    clf.fit(training_input, target)
    
  4. 让我们先看看哪些特征最重要:

    importances = pd.Series(
        clf.feature_importances_ * 100,
        index=training_input.columns).sort_values(ascending=False)
    importances
    

以下是按重要性排名的特征:

uniformity_cell_size           83.972870
uniformity_cell shape           7.592903
bare_nuclei                     4.310045
clump_thickness                 4.124183
marginal_adhesion               0.000000
single_epithelial_cell_size     0.000000
bland_chromatin                 0.000000
normal_nucleoli                 0.000000
mitoses                         0.000000

记住,这只是探索性分析。在下一个配方中,我们将尝试生成更可靠的排名。底部特征为零的原因是我们要求最大深度为 3,在这种情况下,可能并非所有特征都会被使用。

  1. 我们可以对实现的准确性进行一些原生分析:

    100 * clf.score(training_input, target)
    

我们得到的性能为 96%。我们不应该用训练集测试算法,因为这会导致结果较为循环。我们将在下一个配方中重新审视这一点。

  1. 最后,让我们绘制决策树:

    fig, ax = plt.subplots(1, dpi=300)
    tree.plot_tree(clf,ax=ax, feature_names=training_input.columns, class_names=['Benign', 'Malignant'])
    

这会产生以下输出:

图 10.5 - 乳腺癌数据集的决策树

图 10.5 - 乳腺癌数据集的决策树

让我们从根节点开始看:它的标准是uniformity_cell_size < 2.5,分类为良性。分裂树的主要特征是细胞大小的一致性。根节点的良性分类仅仅是因为数据集中的大多数样本都是良性的。现在看一下根节点右侧的节点:它有 265 个样本,其中大部分是恶性的,并且标准为uniformity_cell_shape < 2.5

这些规则帮助你初步理解可能驱动数据集的因素。决策树的精确度不是很高,所以将这些视为你的初步步骤。

使用随机森林预测乳腺癌结果

现在我们将使用随机森林预测一些患者的结果。随机森林是一种集成方法(它将使用多个机器学习算法的实例),通过多个决策树得出关于数据的稳健结论。我们将使用与前一个例子相同的例子:乳腺癌特征和结果。

这个例子有两个主要目标:介绍随机森林及机器学习算法训练中的问题。

准备开始

这个例子的代码可以在Chapter10/Random_Forest.py找到。

如何操作...

看一下代码:

  1. 我们像前一个例子一样,首先去除缺失信息的样本:

    import pandas as pd
    import numpy as np
    import pandas as pd
    from sklearn.ensemble import RandomForestClassifier
    from sklearn.model_selection import train_test_split
    from sklearn.tree import export_graphviz
    f = open('breast-cancer-wisconsin.data')
    w = open('clean.data', 'w')
    for line in f:
        if line.find('?') > -1:
            continue
        w.write(line)
    f.close()
    w.close()
    
  2. 现在我们加载清理过的数据:

    column_names = [
        'sample_id', 'clump_thickness', 'uniformity_cell_size',
        'uniformity_cell shape', 'marginal_adhesion',
        'single_epithelial_cell_size', 'bare_nuclei',
        'bland_chromatin', 'normal_nucleoli', 'mitoses',
        'class'
    ]
    samples = pd.read_csv('clean.data', header=None, names=column_names, index_col=0)
    samples
    
    
  3. 我们将数据分为特征和结果:

    training_input = samples.iloc[:, :-1]
    target = samples.iloc[:, -1]
    
  4. 我们创建一个分类器并将数据拟合到它:

    clf = RandomForestClassifier(max_depth=3, n_estimators=200)
    clf.fit(training_input, target)
    

这里最重要的参数是n_estimators:我们要求构建一个由 200 棵树组成的森林。

  1. 现在我们按重要性对特征进行排序:

    importances = pd.Series(
        clf.feature_importances_ * 100,
        index=training_input.columns).sort_values(ascending=False)
    importances
    

以下是输出:

uniformity_cell_size           30.422515
uniformity_cell shape          21.522259
bare_nuclei                    18.410346
single_epithelial_cell_size    10.959655
bland_chromatin                 9.600714
clump_thickness                 3.619585
normal_nucleoli                 3.549669
marginal_adhesion               1.721133
mitoses                         0.194124

结果是非确定性的,意味着你可能会得到不同的结果。另外,请注意,随机森林与前一个例子中的决策树有很大的不同。这是预期之中的,因为决策树是一个单一的估计器,而随机森林权衡了 200 棵树,因此更加可靠。

  1. 我们可以对这个案例进行评分:

    clf.score(training_input, target)
    

我得到的结果是 97.95%。你可能会得到稍微不同的值,因为算法是随机的。正如我们在前一个例子中所说,从训练集获取得分是一个循环过程,远非最佳实践。

  1. 为了更真实地了解算法的准确性,我们需要将数据分为两部分——训练集和测试集:

    for test_size in [0.01, 0.1, 0.2, 0.5, 0.8, 0.9, 0.99]:
        X_train, X_test, y_train, y_test = train_test_split(
            trainning_input, target, test_size=test_size)
        tclf = RandomForestClassifier(max_depth=3)
        tclf.fit(X_train, y_train)
        score = tclf.score(X_test, y_test)
        print(f'{1 - test_size:.1%} {score:.2%}')
    

输出结果如下(记住你会得到不同的值):

99.0% 71.43%
90.0% 94.20%
80.0% 97.81%
50.0% 97.66%
20.0% 96.89%
10.0% 94.80%
1.0% 92.02%

如果你仅用 1%的数据进行训练,你的准确率只有 71%,而如果你使用更多的数据,准确率会超过 90%。注意,准确率并不随着训练集的大小单调增加。决定训练集的大小是一个复杂的问题,涉及许多因素,可能会导致意想不到的副作用。

还有更多...

我们仅仅触及了训练和测试机器学习算法的表面。例如,监督数据集通常会被分成 3 部分,而不是 2 部分(训练、测试和交叉验证)。在训练算法时,你需要考虑许多其他问题,还有更多种类的算法。在本章中,我们试图培养理解机器学习的基本直觉,但如果你打算继续这条路线,这只是你的起点。

第十二章:使用 Dask 和 Zarr 进行并行处理

生物信息学数据集正在以指数速度增长。基于标准工具(如 Pandas)的数据分析策略假设数据集能够装入内存(尽管会有一些外部存储分析的处理),或者假设单台机器能够高效地处理所有数据。不幸的是,这对于许多现代数据集来说并不现实。

在本章中,我们将介绍两种能够处理非常大数据集和昂贵计算的库:

  • Dask 是一个支持并行计算的库,可以扩展到从单台计算机到非常大的云环境和集群环境。Dask 提供了与 Pandas 和 NumPy 类似的接口,同时允许你处理分布在多台计算机上的大数据集。

  • Zarr 是一个存储压缩和分块多维数组的库。正如我们将看到的,这些数组专为处理在大型计算机集群中处理的大数据集而设计,同时在需要时也能在单台计算机上处理数据。

我们的食谱将使用蚊子基因组学的数据介绍这些高级库。你应该将这段代码作为起点,帮助你走上处理大数据集的道路。大数据集的并行处理是一个复杂的话题,而这只是你旅程的开始——而非结束。

因为所有这些库对于数据分析都至关重要,如果你正在使用 Docker,它们都可以在 tiagoantao/bioinformatics_dask Docker 镜像中找到。

在本章中,我们将介绍以下食谱:

  • 使用 Zarr 读取基因组学数据

  • 使用 Python 多进程进行数据并行处理

  • 使用 Dask 基于 NumPy 数组处理基因组数据

  • 使用 dask.distributed 调度任务

使用 Zarr 读取基因组学数据

Zarr (zarr.readthedocs.io/en/stable/) 将基于数组的数据(如 NumPy)存储在磁盘和云存储的层次结构中。Zarr 用来表示数组的数据结构不仅非常紧凑,而且还支持并行读取和写入,这一点我们将在接下来的食谱中看到。在本食谱中,我们将读取并处理来自按蚊基因组 1000 基因组计划的数据(malariagen.github.io/vector-data/ag3/download.xhtml)。在这里,我们将仅进行顺序处理,以便引入 Zarr;在接下来的食谱中,我们将进行并行处理。我们的项目将计算单一染色体上所有基因位置的缺失数据。

准备工作

按照 cloud.google.com/storage/docs/gsutil_install 上提供的指导,从 gsutil 中获取按蚊 1000 基因组数据。在安装了 gsutil 后,使用以下代码行下载数据(约 2 千兆字节 (GB)):

mkdir -p data/AG1000G-AO/
gsutil -m rsync -r \
         -x '.*/calldata/(AD|GQ|MQ)/.*' \
         gs://vo_agam_release/v3/snp_genotypes/all/AG1000G-AO/ \
         data/AG1000G-AO/ > /dev/null

我们从项目中下载了一个样本子集。下载数据后,处理它的代码可以在Chapter11/Zarr_Intro.py中找到。

如何操作...

查看以下步骤以开始:

  1. 让我们首先检查一下 Zarr 文件中提供的结构:

    import numpy as np
    import zarr 
    mosquito = zarr.open('data/AG1000G-AO')
    print(mosquito.tree())
    

我们从打开 Zarr 文件开始(正如我们很快会看到的,这可能实际上并不是一个文件)。之后,我们会打印出里面可用的数据树:

/
├── 2L
│   └── calldata
│       └── GT (48525747, 81, 2) int8
├── 2R
│   └── calldata
│       └── GT (60132453, 81, 2) int8
├── 3L
│   └── calldata
│       └── GT (40758473, 81, 2) int8
├── 3R
│   └── calldata
│       └── GT (52226568, 81, 2) int8
├── X
│   └── calldata
│       └── GT (23385349, 81, 2) int8
└── samples (81,) |S24

Zarr 文件包含五个数组:四个对应蚊子的染色体——2L2R3L3RXY不包括在内)——另一个包含文件中包含的 81 个样本。最后一个数组包含样本名称——我们在这个文件中有 81 个样本。染色体数据由 8 位整数(int8)组成,样本名称则是字符串。

  1. 现在,让我们探索2L染色体的数据。首先来看一些基本信息:

    gt_2l = mosquito['/2L/calldata/GT']
    gt_2l
    

这是输出:

<zarr.core.Array '/2L/calldata/GT' (48525747, 81, 2) int8>

我们有一个包含4852547个 SNP 和81个样本的数组。对于每个 SNP 和样本,我们有2个等位基因。

  1. 现在我们来检查数据是如何存储的:

    gt_2l.info
    

输出看起来是这样的:

Name               : /2L/calldata/GT
Type               : zarr.core.Array
Data type          : int8
Shape              : (48525747, 81, 2)
Chunk shape        : (300000, 50, 2)
Order              : C
Read-only          : False
Compressor         : Blosc(cname='lz4', clevel=5, shuffle=SHUFFLE, blocksize=0)
Store type         : zarr.storage.DirectoryStore
No. bytes          : 7861171014 (7.3G)
No. bytes stored   : 446881559 (426.2M)
Storage ratio      : 17.6
Chunks initialized : 324/324

这里有很多内容需要解析,但现在我们将专注于存储类型、存储的字节数和存储比率。Store type的值是zarr.storage.DirectoryStore,所以数据并不在一个单独的文件中,而是存储在一个目录内。数据的原始大小是7.3 GB!但是 Zarr 使用压缩格式,将数据的大小压缩到426.2 17.6

  1. 让我们来看看数据是如何存储在目录中的。如果你列出AG1000G-AO目录的内容,你会发现以下结构:

    .
    ├── 2L
    │   └── calldata
    │       └── GT
    ├── 2R
    │   └── calldata
    │       └── GT
    ├── 3L
    │   └── calldata
    │       └── GT
    ├── 3R
    │   └── calldata
    │       └── GT
    ├── samples
    └── X
        └── calldata
            └── GT
    
  2. 如果你列出2L/calldata/GT目录的内容,你会发现很多文件在编码该数组:

    0.0.0
    0.1.0
    1.0.0
    ...
    160.0.0
    160.1.0
    

2L/calldata/GT目录中有 324 个文件。记住,在前一步中我们有一个叫做Chunk shape的参数,它的值是(300000, 50, 2)

Zarr 将数组拆分成多个块——这些块比加载整个数组更容易在内存中处理。每个块包含 30000x50x2 个元素。考虑到我们有 48525747 个 SNP,我们需要 162 个块来表示这些 SNP 的数量(48525747/300000 = 161.75),然后乘以 2 以表示样本的数量(81 个样本/每块 50 个 = 1.62)。因此,我们最终会得到 162*2 个块/文件。

提示

分块是一个广泛应用的技术,用于处理不能完全一次性加载到内存中的数据。这包括许多其他库,如 Pandas 或 Zarr。稍后我们将看到一个 Zarr 的例子。更大的观点是,你应该意识到分块的概念,因为它在许多需要大数据的场景中都有应用。

  1. 在我们加载 Zarr 数据进行处理之前,先创建一个函数来计算一个块的基本基因组统计信息。我们将计算缺失值、祖先纯合子数量和异合子数量:

    def calc_stats(my_chunk):
        num_miss = np.sum(np.equal(my_chunk[:,:,0], -1), axis=1)
        num_anc_hom = np.sum(
            np.all([
                np.equal(my_chunk[:,:,0], 0),
                np.equal(my_chunk[:,:,0], my_chunk[:,:,1])], axis=0), axis=1)
        num_het = np.sum(
            np.not_equal(
                my_chunk[:,:,0],
                my_chunk[:,:,1]), axis=1)
        return num_miss, num_anc_hom, num_het
    

如果你查看前面的函数,你会注意到没有任何与 Zarr 相关的内容:它只是 NumPy 代码。Zarr 有一个非常轻量的应用程序接口API),它将 NumPy 中的大多数数据暴露出来,使得如果你熟悉 NumPy,它非常容易使用。

  1. 最后,让我们遍历数据——也就是遍历我们的数据块来计算统计信息:

    complete_data = 0
    more_anc_hom = 0
    total_pos = 0
    for chunk_pos in range(ceil(max_pos / chunk_pos_size)):
        start_pos = chunk_pos * chunk_pos_size
        end_pos = min(max_pos + 1, (chunk_pos + 1) * chunk_pos_size)
        my_chunk = gt_2l[start_pos:end_pos, :, :]
        num_samples = my_chunk.shape[1]
        num_miss, num_anc_hom, num_het = calc_stats(my_chunk)
        chunk_complete_data = np.sum(np.equal(num_miss, 0))
        chunk_more_anc_hom = np.sum(num_anc_hom > num_het)
        complete_data += chunk_complete_data
        more_anc_hom += chunk_more_anc_hom
        total_pos += (end_pos - start_pos)
    print(complete_data, more_anc_hom, total_pos)
    

大多数代码负责管理数据块,并涉及算术运算来决定访问数组的哪部分。就准备好的 Zarr 数据而言,重要的部分是my_chunk = gt_2l[start_pos:end_pos, :, :]这一行。当你切片 Zarr 数组时,它会自动返回一个 NumPy 数组。

提示

在将数据加载到内存时要非常小心。记住,大多数 Zarr 数组的大小将远大于你可用的内存,因此如果尝试加载,可能会导致应用程序甚至计算机崩溃。例如,如果你执行all_data = gt_2l[:, :, :],你将需要大约 8 GB 的空闲内存来加载它——正如我们之前看到的,数据大小为 7.3 GB。

还有更多...

Zarr 具有比这里展示的更多功能,虽然我们将在接下来的示例中探索一些其他功能,但仍有一些可能性是你应该了解的。例如,Zarr 是少数几个允许并发写入数据的库之一。你还可以更改 Zarr 表示的内部格式。

正如我们在这里看到的,Zarr 能够以非常高效的方式压缩数据——这是通过使用 Blosc 库实现的(www.blosc.org/)。由于 Blosc 的灵活性,你可以更改 Zarr 数据的内部压缩算法。

另见

Zarr 有替代格式——例如,分层数据格式 5HDF5)(en.wikipedia.org/wiki/Hierarchical_Data_Format)和网络公共数据格式NetCDF)(en.wikipedia.org/wiki/NetCDF)。虽然这些格式在生物信息学领域之外更为常见,但它们功能较少——例如,缺乏并发写入功能。

使用 Python 多处理进行数据并行处理

处理大量数据时,一种策略是并行处理,以便利用所有可用的中央处理单元CPU)的计算能力,因为现代计算机通常有多个核心。在理论上的最佳情况下,如果你的计算机有八个核心,你可以通过并行处理获得八倍的性能提升。

不幸的是,典型的 Python 代码只能使用一个核心。话虽如此,Python 具有内置功能来利用所有可用的 CPU 资源;事实上,Python 提供了几种方法来实现这一点。在本配方中,我们将使用内置的multiprocessing模块。这里提供的解决方案在单台计算机上运行良好,并且如果数据集能适应内存的话也没有问题,但如果你想要在集群或云端扩展,应该考虑使用 Dask,我们将在接下来的两篇配方中介绍它。

我们在这里的目标仍然是计算与缺失值和杂合度相关的统计信息。

准备工作

我们将使用与之前的配方相同的数据。该配方的代码可以在Chapter11/MP_Intro.py中找到。

如何做...

请按照以下步骤开始:

  1. 我们将使用与之前的配方完全相同的函数来计算统计信息——这是一个高度依赖 NumPy 的函数:

    import numpy as np
    import zarr
    def calc_stats(my_chunk):
        num_miss = np.sum(np.equal(my_chunk[:,:,0], -1), axis=1)
        num_anc_hom = np.sum(
            np.all([
                np.equal(my_chunk[:,:,0], 0),
                np.equal(my_chunk[:,:,0], my_chunk[:,:,1])], axis=0), axis=1)
        num_het = np.sum(
            np.not_equal(
                my_chunk[:,:,0],
                my_chunk[:,:,1]), axis=1)
        return num_miss, num_anc_hom, num_het
    
  2. 让我们访问我们的蚊子数据:

    mosquito = zarr.open('data/AG1000G-AO')
    gt_2l = mosquito['/2L/calldata/GT']
    
  3. 尽管我们使用相同的函数来计算统计信息,但我们对整个数据集的处理方式将有所不同。首先,我们计算所有将调用calc_stats的区间。这些区间将被设计成与变异体的块划分完美匹配:

    chunk_pos_size = gt_2l.chunks[0]
    max_pos = gt_2l.shape[0]
    intervals = []
    for chunk_pos in range(ceil(max_pos / chunk_pos_size)):
        start_pos = chunk_pos * chunk_pos_size
        end_pos = min(max_pos + 1, (chunk_pos + 1) * chunk_pos_size)
        intervals.append((start_pos, end_pos))
    

我们的区间列表必须与磁盘上的块划分相关。这项计算会很高效,只要这个映射尽可能接近。

  1. 现在,我们将把计算每个区间的代码分离到一个函数中。这一点很重要,因为multiprocessing模块将在它创建的每个进程中多次执行这个函数:

    def compute_interval(interval):
        start_pos, end_pos = interval
        my_chunk = gt_2l[start_pos:end_pos, :, :]
        num_samples = my_chunk.shape[1]
        num_miss, num_anc_hom, num_het = calc_stats(my_chunk)
        chunk_complete_data = np.sum(np.equal(num_miss, 0))
        chunk_more_anc_hom = np.sum(num_anc_hom > num_het)
        return chunk_complete_data, chunk_more_anc_hom
    
  2. 我们现在终于将让代码在多个核心上执行:

    with Pool() as p:
        print(p)
        chunk_returns = p.map(compute_interval, intervals)
        complete_data = sum(map(lambda x: x[0], chunk_returns))
        more_anc_hom = sum(map(lambda x: x[1], chunk_returns))
        print(complete_data, more_anc_hom)
    

第一行使用multiprocessing.Pool对象创建一个上下文管理器。Pool对象默认会创建多个编号为os.cpu_count()的进程。池提供了一个map函数,能够在所有创建的进程中调用我们的compute_interval函数。每次调用将处理一个区间。

还有更多...

本配方简要介绍了如何在 Python 中进行并行处理,而无需使用外部库。话虽如此,它展示了并发并行处理的最重要构建块。

由于 Python 中的线程管理方式,线程并不是实现真正并行处理的可行替代方案。纯 Python 代码无法通过多线程并行执行。

一些你可能使用的库——通常 NumPy 就是这样的——能够在执行顺序代码时利用所有底层处理器。确保在使用外部库时,不要过度占用处理器资源:当你有多个进程时,底层库也会使用多个核心。

另见

使用 Dask 处理基于 NumPy 数组的基因组数据

Dask 是一个提供高级并行处理的库,可以从单个计算机扩展到非常大的集群或云操作。它还提供了处理比内存更大的数据集的能力。它能够提供与常见 Python 库如 NumPy、Pandas 或 scikit-learn 相似的接口。

我们将重复之前配方中的一个子集——即计算数据集中 SNP 的缺失情况。我们将使用 Dask 提供的类似于 NumPy 的接口。

在我们开始之前,请注意 Dask 的语义与 NumPy 或 Pandas 等库有很大不同:它是一个懒加载库。例如,当你指定一个等效于 np.sum 的调用时,你实际上并没有计算和求和,而是在未来会计算它的任务。让我们进入配方来进一步澄清这一点。

准备就绪

我们将以一种完全不同的方式重新分块 Zarr 数据。我们这么做的原因是为了在准备算法时能够可视化任务图。包含五个操作的任务图比包含数百个节点的任务图更容易可视化。为了实际目的,你不应该像我们这里做的那样将数据重新分块为如此小的块。实际上,如果你根本不重新分块这个数据集,也是完全可以的。我们这么做只是为了可视化的目的:

import zarr
mosquito = zarr.open('data/AG1000G-AO/2L/calldata/GT')
zarr.array(
    mosquito,
    chunks=(1 + 48525747 // 4, 81, 2),
    store='data/rechunk')

我们最终会得到非常大的块,虽然这对我们的可视化目的很有用,但它们可能太大而无法放入内存中。

此配方的代码可以在 Chapter11/Dask_Intro.py 中找到。

如何实现...

  1. 让我们首先加载数据并检查 DataFrame 的大小:

    import numpy as np
    import dask.array as da
    
    mosquito = da.from_zarr('data/rechunk')
    mosquito
    

如果你在 Jupyter 中执行,这将是输出结果:

图 11.1 - Dask 数组的 Jupyter 输出,汇总我们的数据

图 11.1 - Dask 数组的 Jupyter 输出,汇总我们的数据

完整的数组占用 7.32 GB。最重要的数字是块的大小:1.83 GB。每个工作节点需要有足够的内存来处理一个块。记住,我们这里只是使用了较少的块数,以便能够在这里绘制任务。

由于大块数据的大小,我们最终只得到了四个块。

我们尚未将任何内容加载到内存中:我们只是指定了最终想要执行的操作。我们正在创建一个任务图来执行,而不是立即执行——至少目前如此。

  1. 让我们来看看我们需要执行哪些任务来加载数据:

    mosquito.visualize()
    

这是输出结果:

图 11.2 - 加载我们的 Zarr 数组所需执行的任务

图 11.2 - 加载我们的 Zarr 数组所需执行的任务

因此,我们有四个任务要执行,每个块对应一个任务。

  1. 现在,让我们看看计算每个块缺失值的函数:

    def calc_stats(variant):
        variant = variant.reshape(variant.shape[0] // 2, 2)
        misses = np.equal(variant, -1)
        return misses
    

每个块的函数将在 NumPy 数组上操作。请注意区别:我们在主循环中使用的代码是针对 Dask 数组的,但在块级别,数据以 NumPy 数组的形式呈现。因此,这些块必须适配内存,因为 NumPy 需要如此。

  1. 后面,当我们实际使用这个函数时,我们需要一个二维2D)数组。由于数组是三维3D)的,我们需要对数组进行重塑:

    mosquito_2d = mosquito.reshape(
        mosquito.shape[0],
        mosquito.shape[1] * mosquito.shape[2])
    mosquito_2d.visualize()
    

这是当前的任务图:

图 11.3 - 加载基因组数据并重塑的任务图

图 11.3 - 加载基因组数据并重塑的任务图

reshape 操作发生在 dask.array 层,而不是 NumPy 层,因此它仅向任务图中添加了节点。仍然没有执行。

  1. 现在,让我们准备执行这个函数——意味着在整个数据集上向我们的任务图中添加任务。有很多种执行方式;在这里,我们将使用 dask.array 提供的 apply_along_axis 函数,它基于 NumPy 中同名的函数:

    max_pos = 10000000
    stats = da.apply_along_axis(
        calc_stats, 1, mosquito_2d[:max_pos,:],
        shape=(max_pos,), dtype=np.int64)
    stats.visualize()
    

目前,我们只打算研究前百万个位置。正如你在任务图中看到的,Dask 足够智能,只会对参与计算的块添加操作:

图 11.4 - 包括统计计算在内的完整任务图

图 11.4 - 包括统计计算在内的完整任务图

  1. 记住,在此之前我们还没有进行任何计算。现在是时候真正执行任务图了:

    stats = stats.compute() 
    

这将启动计算。计算的具体方式是我们将在下一个配方中讨论的内容。

警告

由于块大小的问题,这段代码可能会导致你的计算机崩溃。至少需要 16 GB 内存才能保证安全。记住,你可以使用更小的块大小——而且你应该使用更小的块大小。我们之所以使用这样的块大小,是为了能够生成前面展示的任务图(否则,它们可能会有数百个节点,无法打印出来)。

还有更多内容...

我们没有在这里讨论如何优化 Dask 代码的策略——那将是另一本书的内容。对于非常复杂的算法,你需要进一步研究最佳实践。

Dask 提供的接口类似于其他常见的 Python 库,如 Pandas 或 scikit-learn,可以用于并行处理。你也可以将它用于不依赖现有库的通用算法。

参见

使用 dask.distributed 调度任务

Dask 在执行方面非常灵活:我们可以在本地执行、在科学集群上执行,或者在云上执行。这种灵活性是有代价的:它需要被参数化。有多种配置 Dask 调度和执行的方式,但最通用的是 dask.distributed,因为它能够管理不同种类的基础设施。因为我不能假设你能够访问像 dask.distributed 这样存在于不同平台上的集群或云服务。

在这里,我们将再次计算 Anopheles 1000 基因组项目的不同变体的简单统计数据。

准备工作

在开始使用dask.distributed之前,我们需要注意,Dask 有一个默认的调度器,这个调度器实际上会根据你所使用的库而有所变化。例如,以下是我们 NumPy 示例的调度器:

import dask
from dask.base import get_scheduler
import dask.array as da
mosquito = da.from_zarr('data/AG1000G-AO/2L/calldata/GT')
print(get_scheduler(collections=[mosquito]).__module__)

输出将如下所示:

dask.threaded

Dask 在这里使用了一个线程调度器。对于 NumPy 数组来说,这样做是有道理的:NumPy 实现本身是多线程的(真正的多线程,带有并行性)。当底层库并行运行时,我们不希望有大量进程在后台运行。如果你使用的是 Pandas DataFrame,Dask 可能会选择一个多进程调度器。因为 Pandas 本身不支持并行,所以让 Dask 自己并行运行是有意义的。

好的——既然我们已经解决了这个重要细节,现在让我们回到环境准备工作。

dask.distributed 有一个集中式调度器和一组工作节点,我们需要启动它们。可以在命令行中运行以下代码来启动调度器:

dask-scheduler --port 8786 --dashboard-address 8787

我们可以在与调度器相同的机器上启动工作节点,方法如下:

dask-worker --nprocs 2 –nthreads 1 127.0.0.1:8786

我指定了每个进程使用一个线程。对于 NumPy 代码来说,这个配置是合理的,但实际配置将取决于你的工作负载(如果你在集群或云上,配置可能完全不同)。

小贴士

你实际上不需要像我这里所做的那样手动启动整个进程。dask.distributed 会为你启动一些东西——虽然它不会完全优化你的工作负载——如果你没有自己准备好系统(详情请见下一部分)。但我想给你一个概念,因为在很多情况下,你必须自己设置基础设施。

同样,我们将使用第一部分食谱中的数据。请确保按照准备工作部分的说明下载并准备好数据。我们不会使用重新分块的部分——我们将在下一部分的 Dask 代码中进行处理。我们的代码可以在Chapter11/Dask_distributed.py中找到。

如何做到这一点...

按照以下步骤开始:

  1. 让我们从连接到之前创建的调度器开始:

    import numpy as np
    import zarr
    import dask.array as da
    from dask.distributed import Client
    
    client = Client('127.0.0.1:8786')
    client
    

如果你使用的是 Jupyter,你将看到一个很好的输出,汇总了你在此食谱的准备工作部分所创建的配置:

图 11.5 - 使用 dask.distributed 时的执行环境摘要

图 11.5 - 使用 dask.distributed 时的执行环境摘要

你会注意到这里提到了一个仪表板。dask.distributed 提供了一个实时仪表板,允许你跟踪计算的状态。你可以在浏览器中输入 http://127.0.0.1:8787/ 来访问它,或者直接点击 图 11.5 中提供的链接。

由于我们还没有进行任何计算,仪表板大部分是空的。一定要探索顶部的许多标签:

图 11.6 - dask.distributed 仪表板的初始状态

图 11.6 - dask.distributed 仪表板的初始状态

  1. 让我们加载数据。更严格地说,让我们准备任务图以加载数据:

    mosquito = da.from_zarr('data/AG1000G-AO/2L/calldata/GT')
    mosquito
    

以下是在 Jupyter 上的输出:

图 11.7 - 原始 Zarr 数组(2L 染色体)的汇总

图 11.7 - 原始 Zarr 数组(2L 染色体)的汇总

  1. 为了方便可视化,让我们再次进行分块。我们还将为第二个维度——样本——创建一个单一的块。这是因为我们缺失值的计算需要所有样本,而在我们的特定情况下,为每个样本创建两个块是没有意义的:

    mosquito = mosquito.rechunk((mosquito.shape[0]//8, 81, 2))
    

提醒一下,我们有非常大的块,你可能会遇到内存问题。如果是这样,你可以使用原始的块进行运行。只是可视化效果将无法读取。

  1. 在继续之前,让我们要求 Dask 不仅执行重新分块操作,还要确保结果已经准备好并存储在工作节点中:

    mosquito = mosquito.persist()
    

persist 调用确保数据在工作节点中可用。在以下截图中,你可以看到计算过程中的仪表板。你可以查看每个节点上正在执行的任务、已完成和待完成的任务摘要,以及每个工作节点上存储的字节数。需要注意的是 溢写到磁盘 的概念(见屏幕左上角)。如果内存不足以容纳所有块,它们会暂时写入磁盘:

图 11.8 - 执行持久化函数以重新分块数组时的仪表板状态

图 11.8 - 执行持久化函数以重新分块数组时的仪表板状态

  1. 现在,让我们计算统计信息。对于最后一个配方,我们将使用不同的方法——我们将请求 Dask 对每个块应用一个函数:

    def calc_stats(my_chunk):
        num_miss = np.sum(
            np.equal(my_chunk[0][0][:,:,0], -1),
            axis=1)
        return num_miss
    stats = da.blockwise(
        calc_stats, 'i', mosquito, 'ijk',
        dtype=np.uint8)
    stats.visualize()
    

请记住,每个块不是 dask.array 实例,而是一个 NumPy 数组,因此代码是在 NumPy 数组上运行的。以下是当前的任务图。没有加载数据的操作,因为之前执行的函数已经完成了所有这些操作:

图 11.9 - 从持久化数据开始的每个块对  函数的调用

图 11.9 - 从持久化数据开始的每个块对 calc_stats 函数的调用

  1. 最后,我们可以得到我们的结果:

    stat_results = stats.compute()
    

还有更多...

关于 dask.distributed 接口,还有很多内容可以进一步讲解。在这里,我们介绍了其架构的基本概念和仪表盘。

dask.distributed 提供了基于 Python 标准 async 模块的异步接口。由于本章内容的介绍性性质,我们不会详细讨论它,但建议你查看相关内容。

另见

posted @ 2025-10-23 15:20  绝不原创的飞龙  阅读(0)  评论(0)    收藏  举报