Python-生物信息学秘籍-全-

Python 生物信息学秘籍(全)

原文:Bioinformatics with Python Cookbook

协议:CC BY-NC-SA 4.0

零、前言

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

这本更新版的《Python 生物信息学指南》从 Python 生态系统中各种工具和库的快速概述开始,这些工具和库将帮助您转换、分析和可视化生物数据集。随着章节的深入,您将借助真实世界的例子,涵盖下一代测序、单细胞分析、基因组学、宏基因组学、群体遗传学、系统发育学和蛋白质组学的关键技术。您将学习如何使用重要的管道系统,如 Galaxy 服务器和 Snakemake,并了解 Python 中用于函数式和异步编程的各种模块。这本书还将帮助您探索一些主题,如在高性能计算框架下使用统计方法发现 SNP,包括 Dask 和 Spark,以及机器学习算法在生物信息学中的应用。

在这本生物信息学 Python 书籍结束时,你将具备实现最新编程技术和框架的知识,使你能够处理各种规模的生物信息学数据。

这本书是给谁的

这本书是为想要解决中高级生物学和生物信息学问题的生物信息学分析师、数据科学家、计算生物学家、研究人员和 Python 开发者而写的。Python 编程语言的工作知识是预期的。生物学基础知识会有帮助。

这本书涵盖了什么

第一章 ,Python 及周边软件生态,告诉你如何用 Python 搭建现代生物信息学环境。本章讨论了如何使用 Docker 部署软件、与 R 接口以及与 Jupyter 笔记本交互。

第二章 ,了解 NumPy、pandas、Arrow、Matplotlib ,介绍数据科学的基础 Python 库:数组和矩阵处理的 NumPy;Pandas 用于基于表格的数据操作;优化 Pandas 处理和 Matplotlib 图表的箭头。

第三章 ,下一代测序,提供处理下一代测序数据的具体解决方案。本章教你如何处理大的 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 熊猫 Matplolib | | | 生物 python | | | Dask,zarr,scikit-learn | |

如果你使用的是这本书的数字版本,我们建议你自己输入代码或者通过 GitHub 库获取代码(链接见下一节)。这样做将帮助您避免任何与复制和粘贴代码相关的潜在错误。

下载示例代码文件

你可以从 GitHub 的 https://GitHub . com/packt publishing/Bioinformatics-with-Python-Cookbook-third-edition 下载本书的示例代码文件。如果代码有更新,它将在现有的 GitHub 库中更新。

我们在github.com/PacktPublishing/也有丰富的书籍和视频目录中的其他代码包。看看他们!

下载彩色图片

我们还提供了一个 PDF 文件,其中有本书中使用的截图/图表的彩色图像。你可以在这里下载:packt.link/3KQQO

使用的惯例

本书通篇使用了许多文本约定。

Code in text:表示文本中的码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、伪 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

Bold :表示一个新术语、一个重要单词或您在屏幕上看到的单词。例如,菜单或对话框中的单词出现在文本中,如下所示。这里有一个例子:“关于列,参见 第十一章——但是你现在可以安全地忽略它。”

提示或重要注意事项

像这样出现。

章节

在这本书里,你会发现几个经常出现的标题(做好准备怎么做...工作原理...还有更多...参见

要给出如何完成配方的明确说明,请使用以下章节:

准备就绪

本节将告诉您制作方法的内容,并介绍如何设置制作方法所需的任何软件或任何初步设置。

怎么做……

本节包含遵循配方所需的步骤。

它是如何工作的……

这一部分通常包括对前一部分发生的事情的详细解释。

还有更多……

这一部分包含了关于配方的附加信息,以使你对配方有更多的了解。

参见

这个部分提供了一些有用的链接,可以链接到食谱的其他有用信息。

取得联系

我们随时欢迎读者的反馈。

总体反馈:如果您对本书的任何方面有疑问,请在邮件主题中提及书名,并发邮件至 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 与它交互,这是一个 Python/R 桥。此外,我们将探索 IPython 框架(通过 Jupyter Lab)可以给我们带来的优势,以便有效地与 r 接口。这一章将为我们将在本书剩余部分中执行的所有计算生物学奠定基础。

由于不同的用户有不同的需求,我们将介绍两种不同的软件安装方法。一种方法是使用 Anaconda Python(docs.continuum.io/anaconda/)发行版,另一种安装软件的方法是通过 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,因为它已经成为数据科学和生物信息学的事实上的标准。同样,允许你安装来自 https://bioconda.github.io/ 的发行版软件。

准备就绪

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

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

如果你在 Windows 上,你应该考虑升级到 Linux 来进行你的生物信息学工作,因为大多数现代生物信息学软件不能在 Windows 上运行。请注意,macOS 将适用于几乎所有的分析,除非您计划使用计算机集群,这可能是基于 Linux 的。

如果您使用的是 Windows 或 macOS,并且不容易访问 Linux,请不要担心。现代虚拟化软件(如 VirtualBoxDocker )将会拯救你,它将允许你在你的操作系统上安装一个虚拟 Linux。如果您正在使用 Windows,并且决定进行本地化而不使用 Anaconda,那么请谨慎选择库;如果您为所有东西(包括 Python 本身)安装 32 位版本,您可能会更安全。

注意

如果您使用的是 Windows,许多工具将不可用。

生物信息学和数据科学正以极快的速度发展;这不仅仅是炒作,这是现实。安装软件库时,选择版本可能会很棘手。根据您拥有的代码,它可能无法与一些旧版本一起工作,或者甚至无法与新版本一起工作。希望您使用的任何代码都能指出正确的依赖关系——尽管这并不能保证。在本书中,我们将修正所有软件包的精确版本,并且我们将确保代码将与它们一起工作。很自然,代码可能需要与其他包版本一起调整。

为这本书开发的软件可以从 https://github . com/packt publishing/Bioinformatics-with-Python-Cookbook-third-edition 获得。要访问它,您需要安装 Git。习惯 Git 可能是个好主意,因为许多科学计算软件都是用它开发的。

在正确安装 Python 堆栈之前,您需要安装您将与之交互操作的所有外部非 Python 软件。每一章的列表都会有所不同,所有特定章节的包都会在它们各自的章节中解释。幸运的是,从这本书的前几版开始,大多数生物信息学软件已经可以通过 Bioconda 项目获得;因此,安装通常很容易。

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

在下表中,您会发现使用 Python 开发生物信息学最重要的软件列表:

| **名称** | **应用** | **网址** | **目的** | | 木星计划 | 所有章节 | https://jupyter.org/ | 交互式计算 | | 熊猫 | 所有章节 | https://pandas.pydata.org/ | 数据处理 | | NumPy | 所有章节 | http://www.numpy.org/ | 阵列/矩阵处理 | | 我的天啊 | 所有章节 | https://www.scipy.org/ | 科学计算 | | 生物 python | 所有章节 | https://biopython.org/ | 生物信息学图书馆 | | 希伯恩 | 所有章节 | http://seaborn.pydata.org/ | 统计图表库 | | 稀有 | 生物信息学和统计学 | https://www.r-project.org/ | 统计计算语言 | | rpy2 | r 连通性 | https://rpy2.readthedocs.io | r 接口 | | PyVCF | 鼻胃管吸出 | https://pyvcf.readthedocs.io | VCF 加工 | | 我留下来 | 鼻胃管吸出 | https://github.com/pysam-developers/pysam | SAM/BAM 处理 | | HTSeq | NGS/基因组 | https://htseq.readthedocs.io | NGS 加工 | | 树木 | 系统发育学 | 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 实验室。Jupyter 已经成为编写交互式数据分析脚本的事实上的 ?? 标准。不幸的是,Jupyter 笔记本的默认格式是基于 JSON 的。这种格式难以阅读,难以比较,并且需要导出以输入到普通的 Python 解释器中。为了避免这个问题,我们将使用jupytext(jupytext.readthedocs.io/)扩展 Jupyter,这允许我们将 Jupyter 笔记本保存为普通的 Python 程序。

怎么做...

为了让开始,请看下面的步骤:

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

  2. 我们去图书馆吧。我们现在将使用biopython=1.70创建一个名为bioinformatics_base的新conda环境,如下面的命令所示:

    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 :

    conda install rpy2 r-essentials r-gridextra
    

    安装 R

注意r-essentials安装了很多 R 包,包括我们后面会用到的 ggplot2。此外,我们安装了r-gridextra,因为我们将在笔记本中使用它。

还有更多...

如果您不想使用 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 中,我们应该用笔记本打开 jupytext 文件,而不是文本编辑器。由于 jupytext 文件与 Python 文件具有相同的扩展名——这是一个特性,而不是一个错误——默认情况下,JupyterLab 将使用普通的文本编辑器。当我们打开一个 jupytext 文件时,我们需要覆盖缺省值。打开后右键选择笔记本,如下截图所示:

Figure 1.2 – Opening a jupytext file in Notebook

图 1.2–在笔记本中打开一个 jupytext 文件

我们的 jupytext 文件不会保存图形输出,这对于本书来说已经足够了。如果你想要一个带有图像的版本,可以使用配对笔记本。更多详情,请查看 Jupytext 页面(【https://github.com/mwouts/jupytext】??)。

警告

由于我们的代码是要在 Jupyter 中运行的,所以在本书中,我不会多次使用print来输出内容,因为单元格的最后一行会自动呈现。如果你不用笔记本,记得做一个print

用 Docker 安装所需软件

Docker 是实现操作系统级虚拟化最广泛使用的框架。这项技术允许你拥有一个独立的容器:一个比虚拟机更轻的层,但仍然允许你划分软件。这基本上隔离了所有的进程,让人感觉每个容器都是一个虚拟机。

Docker 在开发领域的两个极端都能很好地工作:它是为学习目的设置本书内容的一种便利方式,并且可以成为您在复杂环境中部署应用程序的首选平台。这个配方是前一个配方的替代方案。

然而,对于长期的开发环境来说,遵循前面的方法可能是您的最佳途径,尽管它可能需要更费力的初始设置。

准备就绪

如果你在 Linux 上,你要做的第一件事就是安装 Docker。最安全的解决方案是从 https://www.docker.com/获得最新版本。虽然您的 Linux 发行版可能有一个 Docker 包,但它可能太旧了,而且有很多错误。

如果你是 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/)关于如何暴露端口。

参见

以下也值得了解:

通过 rpy2 与 R 接口

如果有一些你需要的功能,而你在 Python 库中找不到,你的第一个调用端口是检查它的是否在 R 中实现了,对于统计方法,R 仍然是最完整的框架;此外,一些生物信息学功能在 R 中只有才有,并且可能作为属于 Bioconductor 项目的软件包提供。

rpy2 提供了一个从 Python 到 r 的声明性接口。正如你将看到的,你将能够编写非常优雅的 Python 代码来执行接口过程。为了显示接口(并尝试最常见的 R 数据结构之一 DataFrame 和最流行的 R 库之一ggplot2),我们将从人类 1000 基因组计划(【http://www.1000genomes.org/】)下载它的元数据。这不是一本关于 R 的书,但是我们想提供一些有趣且实用的例子。

准备就绪

你需要从 1,000 个基因组序列索引中获取元数据文件。请查看github . com/packt publishing/Bioinformatics-with-Python-Cookbook-third-edition/blob/main/datasets . py,下载sequence.index文件。如果你用的是 Jupyter 笔记本,打开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 数据帧很好地映射到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)
    

    从我们的文件中读取数据

导入后我们做的第一件事是访问read.delim R 函数,它允许您读取文件。R 语言规范允许你在对象名中加点。所以我们要把一个函数名转换成read_delim。然后,我们称函数名为 proper 请注意以下高度声明性的特性。首先,大多数原子对象,比如字符串,可以不经过转换就被传递。其次,参数名可以无缝转换(除了点号问题)。最后,对象在 Python 命名空间中可用(然而,对象实际上在 R 命名空间中不可用;我们稍后将进一步讨论这一点)。

作为参考,我已经包含了相应的 R 代码。我希望这是一个简单的转换。seq_data对象是一个数据帧。如果你知道 basic R 或pandas,你可能知道这种类型的数据结构。如果不是,那么这本质上就是一个表,也就是一系列行,其中每一列都具有相同的类型。

  1. 让我们对该数据帧进行基本检查,如下:

    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. 我们需要多按摩一下这张桌子;这方面的细节可以在笔记本里找到。这里,我们将最终把数据帧转换成 R(记住,虽然它是一个 R 对象,但它实际上在 Python 名称空间上是可见的):

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

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

注意

虽然您可以在 Python 上执行绘图,但 R 具有默认的内置绘图功能(这里我们将忽略)。它还有一个名为 ?? 的库,实现了图形 ?? 的 ?? 语法(一种指定统计图表的声明性语言)。

  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 笔记本中打印图像:

    Image(filename='out.png')
    

生成了以下图表:

Figure 1.3 – The ggplot2-generated histogram of center names, which is responsible for sequencing the lanes of the human genomic data from the 1,000 Genomes Project

图 1.3–gg plot 2 生成的中心名称直方图,负责对来自 1,000 基因组项目的人类基因组数据的泳道进行测序

  1. 作为最后一个示例,我们现在将使用人类 1,000 个基因组项目(我们将彻底使用该项目的数据摘要,可以在 第三章下一代测序的使用现代序列格式*配方中看到)为约鲁班人(YRI)和具有北欧和西欧血统的犹他州居民(CEU)的所有测序泳道绘制一个读计数和碱基计数散点图此外,我们对不同类型测序之间的差异感兴趣(例如,外显子组覆盖、高覆盖和低覆盖)。首先,我们生成一个只有YRICEU通道的数据帧,并限制最大基址和读取计数:

    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()
    

希望这个例子(请参考下面的截图)能让我们清楚地了解图形方法的语法的力量。我们将从声明数据框架和使用的图表类型开始(即由geom_point实现的散点图)。

请注意,表达每个点的形状取决于POPULATION变量,颜色取决于ANALYSIS_GROUP变量是多么容易:

Figure 1.4 – The ggplot2-generated scatter plot with base and read counts for all sequencing lanes read; the color and shape of each dot reflects categorical data (population and the type of data sequenced)

图 1.4–gg plot 2 生成的散点图,带有所有测序泳道读数的碱基和读数计数;每个点的颜色和形状反映了分类数据(群体和排序数据的类型)

  1. 因为 R 数据帧与和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 数据帧(注意,我们转换的是 R 名称空间中的yri_ceu,而不是 Python 名称空间中的那个)。我们在pandas数据帧上删除指示配对 FASTQ 文件名称的列,并将其复制回 R 名称空间。如果您打印新的 R 数据帧的列名,您将会看到PAIRED_FASTQ不见了。

还有更多...

值得重复的是,Python 软件生态的进步正在以极快的速度发生。这意味着,如果某个功能目前还不可用,它可能会在不久的将来发布。因此,如果您正在开发一个新项目,在使用 R 包中的功能之前,一定要检查 Python 前沿的最新发展。

生物导体项目中有大量的生物信息学 R 包(www.bioconductor.org/)。这可能是您在生物信息学功能的 R 世界中的第一个停靠站。不过注意很多 R 生物信息学包并不在 Bioconductor 上,所以一定要在综合 R 档案网 ( CRAN )(参考 CRAN 在cran.rproject.org/)上搜索更广泛的 R 包。

Python 有很多绘图库。Matplotlib 是最常见的库,但是您还有很多其他选择。在 R 的上下文中,值得注意的是有一个类似于 ggplot2 的 Python 实现,它基于图表的图形描述语言的语法,而且——令人惊讶的是——这叫做ggplot!(yhat.github.io/ggpy/)。

参见

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

与朱庇特一起表演魔术

与标准 Python 相比,Jupyter 提供了相当多的额外特性。在这些特性中,它提供了一个名为 magics 的可扩展命令框架(实际上,这只适用于 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 magics 的简洁和优雅:

  1. 你需要做的第一件事是加载 R magics 和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!
    

数据帧的类型不是标准的 Python 对象,而是一个pandas数据帧。这与之前版本的 R magic 界面有所不同。

  1. 因为我们有一个pandas数据框架,我们可以使用pandas接口:

    my_col = list(seq_data.columns).index("CENTER_NAME")
    seq_data['CENTER_NAME'] = seq_data['CENTER_NAME'].apply(lambda` x: x.upper())
    

    很容易地对它进行操作

  2. 让我们把这个数据帧放回 R 名称空间,如下:

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

-i参数通知 magic 系统,Python 空间后面的变量将被复制到 R 名称空间中。第二行只是显示数据帧确实在 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 函数,因为魔法系统会为你处理这些。当你告诉 R 打印一个图表时,它会神奇地出现在你的笔记本或图形控制台上。

还有更多...

随着时间的推移,R magics 的界面似乎已经发生了很大的变化。比如这本书第一版的 R 代码我就更新过几次。DataFrame 赋值的当前版本返回pandas对象,这是一个主要的变化。

参见

有关更多信息,请查看以下链接:

二、了解 NumPy、pandas、Arrow 和 Matplotlib

Python 最大的优势之一是它丰富的高质量科学和数据处理库。所有这些的核心是 NumPy ,它提供了高效的数组和矩阵支持。在 NumPy 上面,我们可以找到几乎所有的科学图书馆。例如,在我们的领域,有生物制药。但是其他通用数据分析库也可以用于我们的领域。例如,熊猫是处理表格数据的事实上的标准。最近, Apache Arrow 提供了一些 pandas 功能的高效实现,以及语言互操作性。最后, Matplotlib 是 Python 空间中最常见的绘图库,适合科学计算。虽然这些是具有广泛适用性的通用库,但它们是生物信息学处理的基础,所以我们将在本章研究它们。

我们将从熊猫开始,因为它提供了一个具有广泛实用性的高级库。然后,我们将介绍 Arrow,我们将只在支持熊猫的范围内使用它。之后,我们将讨论 NumPy,它是我们所做的几乎所有事情背后的主力。最后,我们将介绍 Matplotlib。

我们的食谱是非常入门的——这些图书馆中的每一个都很容易占据一整本书,但是食谱应该足以帮助你阅读这本书。如果您正在使用 Docker,并且因为所有这些库都是数据分析的基础,它们可以在来自 第一章tiagoantao/bioinformatics_base Docker 图像中找到。

在本章中,我们将介绍以下配方:

  • 用熊猫来处理疫苗不良事件
  • 处理加入熊猫数据框架的陷阱
  • 减少熊猫数据帧的内存使用
  • 用 Apache Arrow 加速 pandas 处理
  • 理解 NumPy 是 Python 数据科学和生物信息学背后的引擎
  • 介绍用于图表生成的 Matplotlib

利用熊猫处理疫苗不良事件

我们将用一个具体的生物信息学数据分析例子来介绍熊猫:我们将研究来自疫苗不良事件报告系统 ( VAERSvaers.hhs.gov/)的数据。由美国卫生与公众服务部维护的 VAERS 包括一个追溯到 1990 年的疫苗不良事件数据库。

VAERS 让数据以逗号分隔值 ( CSV )格式可用。 CSV 格式非常简单,甚至可以用简单的文本编辑器(小心非常大的文件,因为它们可能会使您的编辑器崩溃)或 Excel 等电子表格打开。熊猫可以很容易地用这种格式工作。

准备就绪

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

您可以使用文本编辑器随意查看这些文件,或者最好使用诸如less(对于压缩文件使用zless)之类的工具。你可以在vaers . hhs . gov/docs/VAERSDataUseGuide _ en _ September 2021 . 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 绘图机器,它在下面使用 Matplotib。

  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")
    

这包括前面步骤中的两个数字。以下是输出:

Figure 2.1 – Left – the age for each observation of adverse effect;  right – a histogram showing the distribution of ages

图 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。总的来说,对南的解读要慎重。这可能意味着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)
    

在我们获得仅包含死亡的数据帧后,我们必须读取包含疫苗信息的数据。首先要对疫苗的种类及其不良事件做一些探索性分析。以下是节略输出:

 …
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 数据分析生态系统的基础组件。

参见

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

应对加入熊猫数据框架的陷阱

前一个食谱是一个旋风之旅,介绍了熊猫,并展示了我们将在本书中使用的大多数功能。关于熊猫的详尽讨论需要一本完整的书,在这本食谱中,以及在下一本食谱中,我们将讨论影响数据分析的主题,这些主题在文献中很少讨论,但非常重要。

在这份食谱中,我们将讨论一些通过连接处理相关数据帧的陷阱:事实证明,许多数据分析错误是由于不小心连接数据而引入的。我们将在这里介绍减少此类问题的技术。

准备就绪

我们将使用与前一个配方中相同的数据,但是我们将稍微混淆一下,以便我们可以讨论典型的数据分析缺陷。我们将再次把主要不良事件表与疫苗接种表结合起来,但我们将从每个表中随机抽取 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有一个条目。当您离开 join 时,可能会出现左侧重复多次的情况。例如,我们之前做的groupby操作显示 962303 的VAERS_ID有 11 个条目。这是正确的,但是不正确的预期并不少见,即在左侧的每行输出中仍然有一行。这是因为左连接返回 1 个或多个左条目,而上面的内连接返回 0 个或 1 个条目,有时我们希望正好有 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 相关联。

还有更多...

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

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

减少熊猫数据帧的内存使用

当您处理大量信息时——例如,当分析全基因组测序数据时——内存使用可能会成为您分析的一个限制。事实证明,从记忆的角度来看,天真的熊猫并不是很有效率,我们可以大大减少它的消耗。

在这个食谱中,我们将重新访问我们的 VAERS 数据,并研究几种减少熊猫内存使用的方法。这些变化的影响可能是巨大的:在许多情况下,减少内存消耗可能意味着能够使用 pandas 或需要更替代和复杂的方法,如 Dask 或 Spark。

准备就绪

我们将使用第一个配方的数据。如果你已经运行过了,你就万事俱备了;如果没有,请遵循那里讨论的步骤。你可以在Chapter02/Pandas_Memory.py中找到这段代码。

怎么做……

请遵循以下步骤:

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

    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

这里,我们有关于行数和每行的类型和非空值的信息。最后,我们可以看到数据帧需要 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。

作为这种方法的替代,你可以看看熊猫的分类变量。我更喜欢使用它们,因为它们有更广泛的应用。

  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——那是大约 1/3 的数据。那样的话,我们可以跳过它。下面是最终版本:

    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 上的下一个方法,它将允许您节省额外的内存以提高内存效率。
  • 如果您最终得到的数据帧占用的内存超过了您在单台机器上可用的内存,那么您必须加快游戏的速度,使用分块技术——我们不会在 Pandas 的上下文中介绍这一点——或者可以自动处理大量数据的技术。Dask,我们将在第十一章中讨论,Dask 和 Zarr 的并行处理,允许你使用一个类似熊猫的接口来处理大于内存的数据集。

用 Apache Arrow 加速熊猫加工

当处理大量数据时,比如在全基因组测序中,pandas 既慢又耗内存。Apache Arrow 为几个 pandas 操作提供了更快、更节省内存的实现,并且可以与之互操作。

Apache Arrow 是由 pandas 的创始人 Wes McKinney 共同创建的一个项目,它有几个目标,包括以语言无关的方式处理表格数据,这允许语言互操作性,同时提供内存和计算高效的实现。这里,我们只关注第二部分:提高大数据处理的效率。我们将用一种综合的方式来对待熊猫。

这里,我们将再次使用 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 中的一些函数与熊猫有相似的名字(例如,read_csv),但这并不是最常见的。例如,请注意我们计算数据帧总大小的方式:通过获得每一列的大小并执行求和,这是一种不同于 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 也可以创建一个更紧凑的表示。

前面的代码有一个关于内存消耗的问题:当转换器运行时,它将需要内存来保存熊猫和箭头表示,因此违背了使用更少内存的目的。Arrow 可以在创建熊猫版本时自毁其表示,从而解决问题。这条线是vdata = vdata_arrow.to_pandas(self_destruct=True)

还有更多...

如果你有一个熊猫不能处理的非常大的数据帧,甚至在它被 Arrow 加载之后,那么 Arrow 可能会做所有的处理,因为它也有一个计算引擎。也就是说,在撰写本文时,Arrow 的引擎在功能上远不如 pandas 完善。记住 Arrow 还有许多其他的特性,比如语言互操作性,但是我们不会在本书中用到它们。

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

您的大部分分析都会使用 NumPy,即使您没有明确地使用它。NumPy 是一个数组操作库,位于 pandas、Matplotlib、Biopython 和 scikit-learn 等库之后。虽然您的许多生物信息学工作可能不需要直接使用 NumPy,但是您应该知道它的存在,因为它支持您所做的几乎所有事情,即使只是通过其他库间接支持。

在这个菜谱中,我们将使用 VAERS 数据来展示 NumPy 是如何支持我们使用的许多核心库的。这是一个非常简单的关于这个库的介绍,所以你会意识到它的存在,并且它是几乎所有事情的幕后推手。我们的例子将从美国五个有更多不利影响的州提取病例数,将它们分成年龄组:0 到 19 岁,20 到 39 岁,直到 100 到 119 岁。

准备就绪

我们将再次使用第一个食谱中的数据,所以要确保它是可用的。它的代码可以在Chapter02/NumPy.py中找到。

怎么做……

请遵循以下步骤:

  1. 让我们从装载熊猫的数据开始,减少数据,使它只与美国前五个州相关:

    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 矩阵:

Figure 2.2 – US states with largest numbers of adverse effects

图 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
    

请注意,作为熊猫基础的数据是 NumPy 数据(对两个系列的values调用返回 NumPy 类型)。此外,你可能还记得熊猫有像.shape.dtype这样的属性:这些属性的灵感来自于 NumPy,它们的行为是一样的。

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

    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 足够智能,可以遍历不同的数组结构。如果用任意维数的数组表示,这一行将会起作用,并且会完全按照预期的那样工作。

结果如下:

Figure 2.3 – A matrix representing the distribution of vaccine-adverse effects  in the five US states with the most cases

图 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 和熊猫一样,都是基于 NumPy 的。

参见

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

  • NumPy 有比我们在这里讨论的更多的特性。有很多关于它们的书籍和教程。官方文档是一个很好的起点:numpy.org/doc/stable/
  • NumPy 有许多重要的问题需要发现,但可能最重要的问题之一是广播:NumPy 接受不同结构的数组并正确操作的能力。详情请见numpy.org/doc/stable/user/theory.broadcasting.xhtml

引入 Matplotlib 用于图表生成

Matplotlib 是用于生成图表的最常见的 Python 库。还有更现代的选择,比如以网络为中心的散景,但是 Matplotlib 的优势不仅在于它是最广泛可用的和广泛记录的图表库,而且在计算生物学领域,我们想要一个既以网络为中心又以纸张为中心的图表库。这是因为我们的许多图表将提交给科学杂志,这些杂志同样关注这两种格式。Matplotlib 可以为我们处理这些。

这个食谱中的许多例子也可以直接用 pandas 来完成(因此间接用 Matplotlib),但是这里的要点是练习 Matplotlib。

我们将再次使用 VAERS 数据绘制一些关于数据帧元数据的信息,并总结流行病学数据。

准备就绪

同样,我们将使用第一个配方中的数据。代码可以在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")
    

我们首先创建一个带有标题的人物对象。该图将有一个包含条形图的子图。我们还设置了几个标签,并且只使用默认值。这是令人悲伤的结果:

Figure 2.4 – Our first chart attempt, just using the defaults

图 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的坐标。结果如下:

Figure 2.5 – Our second chart attempt, while taking care of the layout

图 2.5–我们的第二次图表尝试,同时注意布局

  1. 现在,我们将根据单个图形上的四个图对我们的数据进行一些汇总分析。我们将绘制涉及死亡的疫苗、施用和死亡之间的天数、随时间推移的死亡人数以及死亡人数最多的 10 个州的性别:

    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. 以下代码同时绘制所有信息。我们将会有四个 2 乘 2 格式的支线剧情:

    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函数返回 figure 对象和四个轴对象,我们可以用它们来创建图表。请注意,图例位于饼图中,我们在时间距离图上使用了双轴,并且我们有一种方法来计算每个州的死亡人数图表上的堆叠条形图。结果如下:

Figure 2.6 – Four combined charts summarizing the vaccine data

图 2.6-总结疫苗数据的四个组合图表

还有更多...

Matplotlib 有两个界面可以使用——一个较老的界面,设计类似于 MATLAB,和一个更强大的面向对象的 ( OO )界面。尽量避免两者混淆。使用面向对象的接口可能更经得起未来的考验。类似 MATLAB 的界面在matplotlib.pyplot模块下面。让事情变得混乱的是,面向对象接口的入口点在那个模块中——也就是说,matplotlib.pyplot.figurematplotlib.pyplot.subplots

参见

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

  • Matplolib 的文档非常非常好。例如,有一个可视化示例库,其中包含生成每个示例的代码的链接。这可以在matplotlib.org/stable/gallery/index.xhtml找到。API 文档通常非常完整。
  • 改善 Matplotlib 图表外观的另一种方法是使用 Seaborn 库。Seaborn 的主要目的是添加统计可视化工件,但作为副作用,当导入时,它会将 Matplotlib 的默认值更改为更容易接受的值。我们将在本书中使用 Seaborn 查看下一章提供的情节。

三、下一代测序

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

在这里,我们不会讨论每一个单独的 NGS 技术本身(这将需要一整本书)。我们将使用现有的 WGS 数据集——1000 个基因组项目——来说明分析基因组数据所需的最常见步骤。这里介绍的配方将很容易适用于其他基因组测序方法。其中一些也可用于转录组分析(例如 RNA-Seq)。这些配方也是独立于物种的,所以你可以将它们应用于任何其他有测序数据的物种。处理不同物种数据的最大差异与基因组大小、多样性和参考基因组的质量有关(如果你的物种存在参考基因组的话)。这些不会对 NGS 处理的自动化 Python 部分产生太大影响。无论如何,我们将在 第五章与基因组一起工作中讨论不同的基因组。

由于这不是一本入门书,所以希望你至少知道什么是 FASTA ( FASTA )、FASTQ、二进制比对图 ( BAM )、以及变体调用格式 ( VCF )文件。我还将使用基本的基因组术语,但不介绍它(如外显子组、非同义突变等)。您需要熟悉基本的 Python。我们将利用这些知识介绍 Python 中的基本库来执行 NGS 分析。在这里,我们将遵循标准生物信息学管道的流程。

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

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

在本章中,我们将介绍以下配方:

  • 访问 GenBank 并在国家生物技术信息中心的数据库中移动
  • 执行基本序列分析
  • 使用现代序列格式
  • 使用路线数据
  • 从 VCF 文件中提取数据
  • 研究基因组可达性,过滤单核苷酸多态性 ( SNP )数据
  • 用 HTSeq 处理 NGS 数据

访问 GenBank 并在 NCBI 数据库中移动

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

由于测序分析是一个很长的主题,并且这本书的目标是中级到高级用户,我们不会对一个核心上不太复杂的主题进行详尽的讨论。

尽管如此,这是我们将在本章末尾看到的更复杂的食谱的一个很好的热身。

准备就绪

我们会使用你在 第一章**Python 以及周边软件生态中安装的 Biopython。Biopython 提供了一个与 NCBI 提供的数据检索系统Entrez的接口。

该配方可在Chapter03/Accessing_Databases.py文件中获得。

小费

你将从 NCBI 访问一个现场应用编程接口 ( API )。请注意,系统的性能在一天中可能会有所不同。此外,在使用它的时候,你应该是一个“好公民”。你会在www.ncbi.nlm.nih.gov/books/NBK25497/#chapter2.找到一些推荐用法指南和要求。值得注意的是,您需要在查询中指定一个电子邮件地址。您应该尽量避免在高峰时段(美国东部时间周一至周五上午 9:00 至下午 5:00)出现大量请求(100 个或更多),并且每秒发布的查询不要超过三个(Biopython 会为您处理这些问题)。这不仅是良好的公民身份,而且如果你过度使用 NCBI 的服务器,你还有被屏蔽的风险(这是给出真实电子邮件地址的好理由,因为 NCBI 可能会试图联系你)。

怎么做...

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

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

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

我们还将模块导入到流程序列中。不要忘记输入正确的电子邮件地址。

  1. 我们将在nucleotide数据库

    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)
    

    Plasmodium falciparum(导致最致命形式疟疾的寄生虫)中寻找氯喹抗性转运蛋白 ( CRT )基因

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

  1. 现在,让我们尝试检索所有这些记录。下面的查询将从 GenBank 下载所有匹配的核苷酸序列,在写这本书的时候是 1374。你可能不会一直想这么做:

    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 数组中检索序列,因此一般来说,开始将比记录上的值早一个,结束将是相等的。坐标系统的问题将在未来的食谱中重新讨论。

对于其他特征类型,我们简单地打印它们。请注意,当您打印时,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 之外的其他生物数据库,比如恩森布尔(【http://www.ensembl.org】)和加州大学圣克鲁斯分校 ( UCSC )基因组生物信息学(genome.ucsc.edu/)。Python 对这些数据库的支持会有很大不同。

如果没有对基本局部比对搜索工具 ( BLAST )的引用,一本关于生物数据库的入门食谱将是不完整的。BLAST 是一种评估序列相似性的算法。NCBI 提供一项服务,允许你将你感兴趣的序列与它自己的数据库进行比较。当然,你可以使用你当地的 BLAST 数据库,而不是使用 NCBI 的服务。Biopython 为此提供了广泛的支持,但由于这太过初级,我将只向您推荐 Biopython 教程。

参见

这些附加信息也很有用:

执行基本序列分析

我们现在将做一些 DNA 序列的基本分析。我们将使用 FASTA 文件并进行一些操作,比如反向互补或转录。和前面的菜谱一样,我们将使用你在 第一章**Python 以及周边软件生态中安装的 Biopython。这两个配方为您提供了必要的介绍性构件,我们将使用它们执行所有现代 NGS 分析,然后在本章和第五章中进行基因组处理

*## 准备就绪

该配方的代码可在Chapter03/Basic_Sequence_Processing.py中找到。我们将以人类的乳糖酶 ( LCT )基因为例;您可以通过使用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 sequence 对象中是可用的,所以让我们首先将它保存到本地磁盘上的 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 基因组计划旨在对世界范围内的人类基因变异进行编目,并利用现代测序技术进行 WGS。该项目公开了所有数据,包括测序仪的输出、序列比对和 SNP 调用,以及许多其他工件。“1000 个基因组”这个名称实际上是用词不当,因为它目前包括了 2500 多个样本。这些样本被分成数百个种群,跨越整个星球。我们将主要使用来自四个人群的数据:非洲约鲁巴人(YRI)拥有北欧和西欧血统的犹他州居民(CEU)在东京的日本人 ( JPT )和在北京的汉族人 ( CHB )。我们选择这些特定人群的原因是他们是第一批来自 HapMap 的人群,hap map 是一个有着相似目标的老项目。他们使用基因分型阵列来发现更多关于这个子集的质量。我们将在 第六章**群体遗传学中重温 1000 个基因组和单体型图项目。

小费

下一代数据集通常非常大。因为我们将使用真实的数据,你将下载的一些文件将会很大。虽然我已经尽可能地选择了最小的实例,但是您仍然需要良好的网络连接和相当大的磁盘空间。等待下载可能是这个食谱中最大的障碍,但是数据管理对 NGS 来说是一个严重的问题。在现实生活中,您将需要为数据传输安排时间、分配磁盘空间(这在财务上可能很昂贵),并考虑备份策略。NGS 最常见的初始错误是认为这些问题微不足道,但事实并非如此。像将一组 BAM 文件复制到网络,甚至复制到您的计算机这样的操作将会变得令人头痛。做好准备。下载大文件后,最起码要检查一下大小是否正确。一些数据库提供消息摘要 5 ( MD5 )校验和。您可以使用 md5sum 等工具将这些校验和与您下载的文件中的校验和进行比较。

下载数据的说明在笔记本的顶部,如Chapter03/Working_with_FASTQ.py的第一个单元格所示。这是一个相当小的文件(27 兆字节 ( 兆字节)),代表一个约鲁巴人女性(NA18489)的部分测序数据。如果你参考 1000 个基因组项目,你会发现绝大多数 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)
    

我们将打开一个 GNU ZIP ( GZIP )文件,这样我们就可以使用 Python 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 s 的分布绘制成距离序列起点的函数:

Figure 3.1 – The number of N calls as a function of the distance from the start of the sequencer read

图 3.1–N 次调用的次数与从序列器读取开始的距离的函数关系

您将看到直到位置25,没有错误。这不是典型序列器输出的结果。我们的示例文件已经被过滤,1000 个基因组过滤规则强制要求在位置25之前不能出现N调用。

虽然我们无法研究位置25之前数据集中N s 的行为(您可以随意使用您自己的未过滤 FASTQ 文件来查看N s 如何在读取位置上分布),但我们可以看到,在位置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 质量分数是准确呼叫概率的对数表示。这个概率给定为。因此,10 的 Q 代表 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功能;我们使用这个只是因为输出看起来比标准的matplotlibboxplot函数稍微好一点。如果你不想依赖seaborn,就使用股票matplotlib功能。这种情况下,你会调用ax.boxplot(vps)而不是sns.boxplot(data=vps, ax=ax)

正如所料,分布并不均匀,如下图所示:

Figure 3.2 – The distribution of Phred scores as a function of the distance from the start of the sequencer read

图 3.2–ph red 分数的分布,作为从测序仪读数开始的距离的函数

还有更多...

尽管不可能讨论序列器文件输出的所有变化,但成对端读取值得一提,因为它们很常见,需要不同的处理方法。使用成对末端测序,DNA 片段的两端都被测序,中间有一个缺口(称为插入片段)。这种情况下会产生两个文件:X_1.FASTQX_2.FASTQ。两个文件将具有相同的顺序和完全相同的序列数。第一个序列将与第一个序列X_2成对出现在X_1中,依此类推。关于编程技术,如果您想保留配对信息,您可以执行如下操作:

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

最后,如果你正在对人类基因组进行测序,你可能想要使用来自完整基因组学的测序数据。在这种情况下,请阅读下一个食谱中的还有更多… 部分,在那里我们将简要讨论完整的基因组数据。

参见

以下是一些包含更多信息的链接:

使用校准数据

在你从测序仪收到你的数据后,你通常会使用工具,如布伦斯-惠勒比对器 ( bwa)将你的序列与参考基因组进行比对。大多数用户都有自己物种的参考基因组。你可以在 第五章使用基因组中阅读更多关于参考基因组的内容。

比对数据最常见的表示是序列比对图 ( 山姆)格式。由于这些文件大部分都很大,你可能会使用它的压缩版本(BAM)。压缩格式是可索引的,用于极快的随机访问(例如,快速找到染色体某一部分的比对)。注意,BAM 文件需要一个索引,通常由 SAMtools 的tabix实用程序创建。SAMtools 可能是使用最广泛的操作 SAM/BAM 文件的工具。

准备就绪

正如在前面的食谱中所讨论的,我们将使用来自 1000 个基因组项目的数据。我们将对女性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 文件对象可以在其记录上迭代。我们将遍历它,直到我们找到一个记录,它的简洁的特质间隙对齐报告 ( 雪茄)字符串包含一个火柴和一个软夹子。

雪茄线给出了单个碱基排列的指示。序列中被剪切的部分是对齐器未能对齐的部分(但没有从序列中去除)。我们还需要读数、其配对 ID 和位置(配对的,因为我们有配对的末端读数),它们被映射到参考基因组。

首先,我们打印查询模板名称,后面跟着引用 ID。引用 ID 是一个指针,指向引用查找表中给定引用的序列名称。举个例子就能说明这一点。对于这个 BAM 文件上的所有记录,引用 ID 是19(一个无信息的数字),但是如果应用bam.getrname(19),就会得到20,这是染色体的名称。因此,不要混淆参考 ID(在本例中为19)和染色体名称(20)。随后是参考起点和参考终点。pysam是从 0 开始的,不是从 1 开始的,所以当你把坐标转换到其他库的时候要小心。您会注意到本例的起点和终点分别是 59,996 和 60,048,这意味着 52 个碱基的比对。当读取大小为 76 时,为什么只有 52 个碱基(还记得这个 BAM 文件中使用的读取大小吗)?答案可以在雪茄串上找到,在我们的例子中是52M24S,它是 52 个碱基的匹配,然后是 24 个碱基的软剪辑。

然后,我们打印对齐的起点和终点,并计算其长度。顺便说一下,你可以通过观察雪茄绳来计算。它从 0 开始(因为读取的第一部分被映射)并在 52 结束。长度又是 76。

现在,我们查询 mate(只有在有成对末端读取的情况下才会这样做)。我们得到它的引用 ID(如前面的代码片段所示)、它的起始位置以及两对之间的距离度量。只有当配偶双方都映射到同一条染色体上时,这种距离的度量才有意义。

然后,我们绘制该序列的 Phred 分数(参考之前的配方,使用现代序列格式,关于 Phred 分数),然后仅绘制比对部分的 ph red 分数。最后,我们打印序列(不要忘记这样做!).这是完整的序列,不是剪辑过的序列(当然可以用前面的坐标来剪辑)。

  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 号位置兆碱基对 ( Mbp )之间的记录。我们将只使用染色体的一小部分。对于这些类型的获取操作,拥有一个索引(由tabix生成)是非常重要的;执行的速度会完全不同。

我们遍历 10 Mbp 边界内的所有记录。对于每个边界,我们得到对齐的开始和结束,并增加对齐的位置之间的可映射性的计数器。最后,我们将其转换为频率,然后绘制出来,如下面的屏幕截图所示:

Figure 3.3 – The percentage of mapped calls as a function of the position from the start of the sequencer read

图 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调用之前的值减去下百分位的值。我们可以使用底部百分位数的列表,但是我们必须修正中间值和顶部,如下所示:

Figure 3.4 – The distribution of Phred scores as a function of the position in the read; the bottom blue color spans from 0 to the 5th percentile; the green color up to the median, red to the 95th percentile, and purple to the maximum

图 3.4–ph red 分数的分布,作为读数中位置的函数;底部的蓝色从 0 到第 5 个百分点;绿色到中间值,红色到第 95 百分位,紫色到最大值

还有更多...

虽然我们将在本章的研究基因组可及性和筛选 SNP 数据方法中讨论数据筛选,但我们的目的不是详细解释 SAM 格式或给出数据筛选的详细过程。这个任务需要一本自己的书,但是有了pysam的基础知识,您可以浏览 SAM/BAM 文件。然而,在本章的最后一个方法中,我们将从 BAM 文件中提取全基因组的指标(通过代表 BAM 文件指标的 VCF 文件上的注释),以了解我们数据集的整体质量。

您可能需要处理非常大的数据文件。某些 BAM 处理可能会花费太多时间。减少计算时间的第一种方法是二次采样。例如,如果以 10%进行二次抽样,就会忽略 10 条记录中的 9 条。对于许多任务,例如为 BAM 文件的质量评估所做的一些分析,百分之十(甚至百分之一)的二次抽样就足以获得文件质量的要点。

如果你使用人类数据,你可能会在 Complete Genomics 得到你的数据。在这种情况下,校准文件会有所不同。尽管 Complete Genomics 提供了转换成标准格式的工具,但如果你使用它自己的数据,你可能会得到更好的服务。

参见

可以在以下链接中找到更多信息:

从 VCF 文件中提取数据

在运行基因型调用者(例如 GATK 或 SAMtools)后,你将有一个 VCF 文件报告关于基因组变异的,比如 SNPs、插入/缺失 ( INDELs )、拷贝数变异 ( CNVs )等等。在这个配方中,我们将通过cyvcf2模块讨论 VCF 加工。

准备就绪

虽然《NGS》讲的都是大数据,但我可以要求你下载多少作为这本书的数据集是有限制的。我相信 2 到 20 GB 的数据对于一个教程来说要求太高了。虽然这个 OOM 中有 1000 个带有真实注释的 VCF 基因组文件,但我们希望在这里使用更少的数据。幸运的是,生物信息学社区已经开发了允许部分下载数据的工具。作为 SAMtools/ htslib包的一部分(【http://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

第一行将部分下载 1000 个基因组项目的 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、INDELs 等,以及每个样本中该变体的状态)。在变异(记录)水平,我们发现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,例如,只接受双等位基因 SNPs)、质量(如您所料,Phred-scaled)和过滤器状态。关于过滤器状态,记住无论 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 默认字典。我们发现这个数据集有 INDELs、CNVs,当然还有 SNPs(大约三分之二是转换,三分之一是颠换)。有剩余数量(79)的三等位基因 SNPs。

还有更多...

本食谱的目的是让你快速掌握cyvcf2模块。在这个阶段,您应该对这个 API 很熟悉了。我们不会在用法细节上花太多时间,因为这将是下一个诀窍的主要目的:使用 VCF 模块来研究你的变体调用的质量。

虽然cyvcf2非常快,但处理基于文本的 VCF 文件仍然需要很多时间。处理这个问题有两个主要策略。一种策略是并行处理,我们将在最后一章 第九章**生物信息管道中讨论。第二个策略是转换成更有效的格式;我们将在第六章 、群体遗传学中提供一个这样的例子。请注意,VCF 开发者正在开发一个二进制变体调用格式 ( BCF )版本来处理这些问题的一部分(www . 1000 genomes . 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/how to-apply-hard-filters-to-a-call-set

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

在第四种情况下,您的 VCF 文件没有所有必需的注释,您必须重新访问您的 BAM 文件(或者甚至其他信息源)。在这种情况下,最好的解决方案是找到尽可能多的额外信息,并用所需的注释创建一个新的 VCF 文件。一些基因型调用者(如 GATK)允许您指定想要的注释;你可能还想使用额外的程序来提供更多的注释。例如,SNP eff(snpeff.sourceforge.net/)会用它们的效果预测来注释你的 SNP(例如,如果它们在外显子中,它们是编码的还是非编码的?).

不可能提供一个明确的方法,因为它会随着你的测序数据类型、你的研究种类、你对错误的容忍度以及其他变量而变化。我们能做的是提供一组典型的分析,以便进行高质量的过滤。

在这个食谱中,我们不会使用来自人类 1,000 基因组计划的数据。我们想要脏的,未过滤的数据,有许多可以用来过滤它的公共注释。我们将使用冈比亚按蚊 1,000 基因组项目(按蚊是一种蚊子媒介,参与传播导致疟疾的寄生虫)的数据,该项目提供过滤和未过滤的数据。你可以在 http://www.malariagen.net/projects/vector/ag1000g找到更多关于这个项目的信息。

我们将得到大约 100 只蚊子的染色体着丝粒的一部分(?? ),随后是该染色体的中间部分(?? )(并标记两者):

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/packt publishing/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 笔记本,记得删除第一行)。在我解释这个函数之前,请注意我们在做什么。

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

window函数将获取一个 VCF 记录(一个非双等位基因透镜(rec.ALT) == 1rec.is_snp SNP),确定该记录所属的窗口(通过按大小执行rec.POS的整数除法),并通过作为fun参数传递给它的函数扩展该窗口的结果列表(在我们的示例中,该参数仅为 1)。

所以,现在我们有一个 100 个元素的列表(每个元素代表 2000 个 bp)。每个元素将是另一个列表,对于找到的每个双等位基因 SNP,该列表将具有 1。

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

  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 个;因此,对它们求和将得出 200。

因此,我们能够以一种明显复杂的方式计算每个窗口中 SNPs 的数量。为什么我们要用这种策略做事,很快就会变得很明显。然而,现在,让我们检查两个文件的计算结果,如下面的截图所示:

Figure 3.5 – The number of biallelic SNP distributed windows of 2,000 bp in size for an area of 200 kilobase pairs (kbp) near the centromere (orange), and in the middle of the chromosome (blue); both areas come from chromosome 3L for circa 100 Ugandan mosquitoes from the Anopheles 1,000 Genomes Project

图 3.5-着丝粒附近(橙色)和染色体中间(蓝色)200 千碱基对(kbp)区域的 2,000 bp 大小的双等位基因 SNP 分布窗口的数量;这两个区域都来自 3L 按蚊 1000 基因组项目中的大约 100 只乌干达蚊子的染色体

小费

注意着丝粒中的 SNPs 数量比染色体中部的少。这是意料之中的,因为在染色体中调用变体比在中间调用更困难。此外,着丝粒中的基因组多样性可能更少。如果你习惯于人类或其他哺乳动物,你会发现变种的密度高得令人讨厌——对你来说就是蚊子!

  1. 让我们看一下样本级注释。我们将检查映射质量零(参见www . broadinstitute . org/gatk/guide/tool docs/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. 现在,让我们打印每个窗口(在本例中,大小为 5000)的中间值和前 75 个百分位数:

    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函数应用。结果看起来像这样:

Figure 3.6 – Median (continuous line) and 75th percentile (dashed) of MQ0 of sample SNPs distributed on windows of 5,000 bp in size for an area of 200 kbp near the centromere (blue) and in the middle of chromosome (green); both areas come from chromosome 3L for circa 100 Ugandan mosquitoes from the Anopheles 1,000 Genomes Project

图 3.6–样本 SNP 的 MQ0 的中位数(实线)和第 75 百分位(虚线),分布在着丝粒附近(蓝色)和染色体中间(绿色)200 kbp 区域的 5,000 bp 大小的窗口上;这两个区域都来自按蚊 1000 基因组项目中大约 100 只乌干达蚊子的 3L 染色体

对于standard.vcf.gz文件,中间值MQ00(它被绘制在最底部,几乎看不见)。这很好,因为它表明大多数与变体命名相关的序列都清晰地映射到基因组的这个区域。对于centro.vcf.gz文件,MQ0质量较差。此外,在一些区域,基因分型者根本找不到任何变异(因此图表不完整)。

  1. 让我们用样本水平的注释 DP 来比较杂合性。这里,我们将对每个 SNP 的样本读取深度 ( DP )绘制杂合性调用的分数。首先,我们将解释结果,然后解释生成它的代码。

下面的屏幕截图显示了在某一深度杂合的呼叫比例:

Figure 3.7 – The continuous line represents the fraction of heterozygosite calls computed at a certain depth; in orange is the centromeric area; in blue is the “standard” area; the dashed lines represent the number of sample calls per depth; both areas come from chromosome 3L for circa 100 Ugandan mosquitoes from the Anopheles 1,000 Genomes Project

图 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 数据框架,或者您可以考虑 PyTables。这里的基本点是有一个足够通用的框架来计算几个样本注释之间的关系。

另外,要注意几个标注的维度空间。例如,如果您的注释是 float 类型的,您可能必须对它进行舍入(否则,您的数据结构的大小可能会变得太大)。

  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')
    

这里,我们将使用两个轴。在左手边,我们将有杂合 SNPs 的分数。在右边,我们会有 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')
    

这里,我们只是为非着丝粒文件打印一个boxplot,如下图所示。结果与预期一致:编码区的 SNP 可能更有深度,因为它们位于比基因间 SNP 更容易调用的更复杂的区域;

Figure 3.8 – Boxplot for the distribution of variant read depth across different SNP effects

图 3.8–不同 SNP 效应的变异阅读深度分布的箱线图

还有更多...

过滤单核苷酸多态性和其他基因组特征的整个问题将需要一本自己的书。这种方法将取决于您拥有的测序数据类型、样本数量和潜在的额外信息(例如,样本中的谱系)。

这份食谱实际上非常复杂,但它的某些部分非常幼稚(在一份简单的食谱中,我能强加给你的复杂性是有限度的)。例如,窗口代码不支持重叠窗口。此外,数据结构过于简单。然而,我希望它们能让你了解处理基因组高通量测序数据的一般策略。更多内容可以在 第四章**高级 NGS 处理中阅读。

参见

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

  • 有许多过滤规则,但我想提醒您注意合理的良好覆盖(明显高于 10 倍)的必要性。参见 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 文件加载到基因组浏览器中以可视化特征。每行至少包括位置信息(染色体、起点和终点)以及可选字段,如名称或链。关于这种形式的全部细节可以在genome.ucsc.edu/FAQ/FAQformat.xhtml#format1找到。

准备就绪

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

我们将从 Ensembl 获取这些信息。去 http://uswest.ensembl.org/Homo_sapiens/Gene/Summary?的db =核心;g = ensg 000000115850并选择导出数据输出格式应为床格式基因信息要选(如果愿意可以多选)。为了方便起见,在Chapter03目录中有一个名为LCT.bed的下载文件。

这个代码的笔记本叫做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)的编码区信息(CCDS)。

您需要安装 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

还有更多...

床的形式可以比这更复杂一点。此外,前面的代码基于关于我们文件内容的非常具体的前提。然而,这个例子应该足以让你开始。即使在最坏的情况下,床的格式也不是很复杂。

HTSeq 有比这多得多的功能,但是这个配方主要是作为整个包的起点。HTSeq 具有一些功能,可以作为我们到目前为止讨论过的大多数食谱的替代方案。*

四、高级 NGS 数据处理

如果你处理下一代测序 ( NGS )数据,你就会知道质量分析和处理是获得结果的两大耗时环节。在本章的第一部分,我们将通过使用包含亲属信息的数据集来更深入地研究 NGS 分析——在我们的例子中,是母亲、父亲和大约 20 个后代。这是执行质量分析的常用技术,因为谱系信息将允许我们对我们的过滤规则可能产生的错误数量进行推断。我们还将利用这个机会,使用相同的数据集,根据现有的注释来寻找基因组特征。

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

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

在本章中,有以下配方:

  • 准备用于分析的数据集
  • 利用孟德尔误差信息进行质量控制
  • 使用标准统计数据探索数据
  • 从测序注释中寻找基因组特征
  • 用 QIIME2 做宏基因组学

准备用于分析的数据集

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

  • 使用更强大的测序技术进行比较——例如,使用桑格测序来验证 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]))
    

我们得到以下输出:

Figure 4.1 - Results from 100,000 simulations: the number of offspring that are heterozygous for certain loci where the mother is AA and the father is heterozygous

图 4.1 -来自 100,000 个模拟的结果:对于母亲是 AA 而父亲是杂合的某些基因座,杂合的后代数量

在这里,我们做了 10 万次模拟。在我的例子中(这是随机的,所以你的结果可能会有所不同),我得到了所有后代都是杂合的零个模拟。事实上,这些都是重复的排列,所以所有都是杂合的概率是或 9.5367431640625 e-07——不太可能。所以,即使对于单个后代,我们可以有 AT 或 AA;对于 20 来说,都是同一类型的可能性很小。这是我们可以用来对孟德尔错误进行不那么幼稚的解释的信息。

  1. 让我们重复母亲和父亲都在的分析:

    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()
    

输出如下所示:

Figure 4.2 - Results from 100,000 simulations: the number of offspring that are AA or heterozygous for a certain locus where both parents are also heterozygous

图 4.2-100,000 次模拟的结果:父母双方都是杂合的特定位点上 AA 或杂合的后代数量

在这种情况下,我们也有重复排列,但我们有四个可能的值,而不是两个:AA,AT,TA 和 TT。我们最终得到所有个体的相同概率为:9.53631640625 e-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 调用。我们在最后打印条目的数量(由于 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. 然后我们加载数据。我们将用熊猫来导航:

    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. 让我们请熊猫展示所有注释的直方图:

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

生成以下直方图:

Figure 4.3 - Histogram of all annotations for a dataset with roughly 50% of errors

图 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)
    

Figure 4.4 - Histogram zooming in on an area of interest for 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)
    

上述代码生成以下输出:

Figure 4.5 - Scatter matrix of annotations of errors for an area of the search space

图 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)
    

输出如下所示:

Figure 4.6 - Scatter matrix of annotations of good calls for an area of the search space

图 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 万个标记。

还有更多……

当丢失 12%的标记时,误差从 5%减少到 0.3%是好还是坏?嗯,那要看你接下来想做什么分析了。也许你的方法对丢失标记有弹性,但不会有太多错误,在这种情况下,这可能会有帮助。但是如果反过来,也许你更喜欢完整的数据集,即使它有更多的错误。如果你应用不同的方法,也许你会在不同的方法中使用不同的数据集。在这个按蚊数据集的具体例子中,数据太多了,减少数据量可能对任何事情都没问题。但是如果你有更少的标记,你将不得不根据标记和质量来评估你的需求。

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

我们将用一个简单的方法来结束本章和本书,这个方法表明,有时你可以从简单的意外结果中学到重要的东西,表面的质量问题可能掩盖重要的生物学问题。

我们将为我们杂交的所有亲本绘制穿过 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 kbp 窗口的中值 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. 图后面的显示了输出:

Figure 4.7 - Median DP per window for all parents of the dataset on chromosome arm 2L

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

你会注意到,对于一些蚊子来说,例如,在第一列和最后一列的蚊子,在染色体臂的中间有一个明显的 DP 下降。其中一些,比如第三列,有一点下降——不那么明显。而对于第二列的底层父代,根本没有下降。

还有更多……

前面的模式有一个生物学原因,最终对测序产生影响:按蚊可能在 2L 臂中部有一个大的染色体倒位。由于进化上的差异,与用于判断的参考基因组不同的核型更难判断。这使得该区域的序列器读取次数减少。这是这个物种特有的,但你可能会认为其他种类的特征也会出现在其他生物身上。

一个更广为人知的案例是拷贝数变异 ( CNV ):如果一个参考基因组只有一个特征的拷贝,但你正在测序的个体有n,那么你可以预期看到一个n倍于整个基因组中值的 DP。

但是,在一般情况下,在整个分析过程中留意奇怪的结果是一个好主意。有时,这是一个有趣的生物特征的标志,就像这里的。或者,这是一个指向错误的指针:例如,主成分分析 ( PCA )可以用来找到错误标记的样本(因为它们可能会聚集在错误的组中)。

使用 QIIME 2 Python API 进行宏基因组学研究

维基百科称宏基因组学是对直接从环境样本中回收的遗传物质的研究。请注意,这里的“环境”应该广义地解释:在我们的例子中,我们将在患有胃肠道问题的儿童的粪便微生物群移植研究中处理胃肠道微生物群。这项研究是 QIIME 2 的教程之一,QIIME 2 是宏基因组学中最广泛使用的数据分析应用程序之一。QIIME 2 有几个接口:一个 GUI、一个命令行和一个称为工件 API 的 Python API。

Tomasz kocióek 有一个使用工件 API 的优秀教程,它基于 QIIME 2 上最完善的(基于客户端的,而不是基于工件的)教程,“移动图片”教程(nb viewer . jupyter . org/gist/tkosciol/29de 5198 a4be 81559 a 075756 c 2490 FDE)。在这里,我们将创建一个 Python 版本的粪便微生物群移植研究,可以在 https://docs.qiime2.org/2022.2/tutorials/fmt/使用客户端界面。你应该熟悉它,因为我们不会在这里深入生物学的细节。我确实遵循比 Tomasz 更复杂的路线:这将允许您对 QIIME 2 Python 内部有更多的了解。在你获得这种体验之后,你很可能会想走托马斯的路线,而不是我的。但是,您在这里获得的经验会让您对 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

您可能希望使用conda install在 QIIME 2 的环境中安装其他软件包。

要为 Jupyter 执行准备,您应该安装 QIIME 2 扩展,如下所示:

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

小费

该扩展具有高度的交互性,允许您从不同的角度查看本书中无法捕捉到的数据。缺点是它在nbviewer中不起作用(一些单元格输出在静态查看器中不可见)。记住与扩展的输出交互,因为许多输出是动态的。

你现在可以开始 Jupyter 了。笔记本可以在Chapter4/QIIME2_Metagenomics.py文件中找到。

警告

由于 QIIME 软件包安装的流动性,我们没有为它提供一个 Docker 环境。这意味着如果你从我们的 Docker 安装开始工作,你将不得不下载配方并手动安装软件包。

您可以找到获取笔记本文件和 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
    

下面是 Juypter 的 QIIME 扩展的一部分输出:

Figure 4.8 - A part of the output of the QIIME2 extension for Jupyter

图 4.8-Jupyter 的 QIIME2 扩展的部分输出

记住扩展是迭代的,提供的信息远不止这张图表。

小费

该配方的原始数据以 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. 让我们使用 dada 2(【https://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 CLI 版本中在线找到结果。

  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 文件的形式提供。

准备就绪

生物基因组的大小差异很大,从 9.7 kbp 的艾滋病毒等病毒,到 9.7 kbp 的大肠杆菌等细菌,到分布在 14 条染色体、线粒体和顶体上的 22 Mbp 的原生动物,如分布在 22 条常染色体、X/Y 染色体和线粒体上的恶性疟原虫等原生动物,到具有三条常染色体、一条线粒体和 X/Y 性染色体的果蝇,再到分布在 22 条常染色体、X/Y 染色体和线粒体上的三对 Gbp 的人类一路上,你有不同的倍性和性染色体组织。

小费

如你所见,不同的生物有非常不同的基因组大小。这种差异可以有几个数量级。这可能会对您的编程风格产生重大影响。处理大基因组需要你在记忆方面更加保守。不幸的是,更大的基因组将受益于速度更高效的编程技术(因为你有更多的数据要分析);这些是相互矛盾的要求。一般的规则是,对于更大的基因组,你必须更加注意效率(速度和内存)。

为了减轻这个食谱的负担,我们将使用来自恶性疟原虫的小真核基因组。这个基因组仍然具有许多较大基因组的典型特征(例如,多条染色体)。因此,这是复杂性和大小之间的一个很好的折衷。请注意,有了像恶性疟原虫那么大的基因组,将整个基因组加载到内存中就有可能进行许多操作。然而,我们选择了一种可以用于更大基因组(例如,哺乳动物)的编程风格,这样您就可以以更通用的方式使用这种方法,但也可以随意对像这样的小基因组使用更占用内存的方法。

我们会使用你在 第一章**Python 以及周边软件生态中安装的 Biopython。像往常一样,这份食谱可以在本书的 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 大小的窗口开始。这对于恶性疟原虫来说是合适的,但是对于染色体数量级与此不同的基因组来说,你需要考虑其他的值。

请注意,我们正在重新读取文件。对于如此小的基因组,对整个基因组进行内存加载是可行的(在步骤 1 )。无论如何,请随意为小基因组尝试这种编程风格——它更快!然而,我们的代码被设计用于更大的基因组。

  1. 注意在for循环中,我们通过解析描述的SO条目忽略了线粒体和顶质体。chrom_sizes字典将保持染色体的大小。

chrom_GC字典是我们最感兴趣的数据结构,它将包含每个 50 kbp 窗口的GC内容的片段的列表。因此,对于大小为 640,851 bp 的染色体 1,将有 14 个条目,因为该染色体的大小为 14 个 50 kbp 的块。

请注意恶性疟原虫基因组的两个不同寻常的特征:基因组富含 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 中不是,在 Python 3 中,keys方法有一个特定的dict_keys返回类型。

我们按顺序画出染色体(因此排序)。我们需要最大染色体的大小(在恶性疟原虫中为 14),以确保染色体的大小以正确的比例打印出来(变量biggest_chrom)。

然后,我们创建一个具有 PNG 输出的有机体的 A4 大小的表示。注意我们画的是非常小的 10 bp 的端粒。这将产生一个类似矩形的染色体。你可以让端粒变大,给它们一个圆形的代表,或者你可能有一个更好的想法,为你的物种使用正确的端粒大小。

我们声明任何低于 17.5%或高于 22.0%的GC含量都将被视为异常值。请记住,对于大多数其他物种来说,这将会高得多。

然后,我们打印这些染色体:它们以端粒为界,由 50 kbp 的染色体片段组成(最后一个片段的大小与余数相同)。每个段将被涂成蓝色,红绿成分基于两个异常值之间的线性归一化。每个染色体片段要么是 50 kbp,要么可能更小,如果它是染色体的最后一个。输出如下图所示:

图 5.2-恶性疟原虫的 14 条染色体,用 GC 含量进行颜色编码(红色超过 22%,黄色低于 17%,蓝色阴影代表两个数字之间的线性梯度)

小费

在 python 成为如此流行的语言之前,Biopython 代码就已经发展了。过去,图书馆的可用性相当有限。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 确实有打印环形基因组的功能,你也可以用它来打印细菌。至于细菌和病毒,这些基因组更容易处理,因为它们非常小。

参见

您可以从以下资源中了解更多信息:

处理低质量的基因组参考

不幸的是,并不是所有的参考基因组都具有恶性疟原虫 ?? 的品质。除了一些模式物种(例如,人类,或常见的果蝇黑腹果蝇和少数其他物种),大多数参考基因组可以使用一些改进。在这个食谱中,我们将学习如何处理低质量的参考基因组。

准备就绪

为了与疟疾主题保持一致,我们将使用两种蚊子的参考基因组,这两种蚊子是疟疾的传播媒介:冈比亚按蚊(这是疟疾最重要的传播媒介,可以在撒哈拉以南非洲找到)和萎缩按蚊(欧洲的一种疟疾传播媒介,虽然这种疾病在欧洲已经被根除,但这种传播媒介仍然存在)。冈比亚按蚊基因组质量合理。大多数染色体已经被绘制出来,尽管 Y 染色体还需要一些工作。有一个相当大的未知染色体,可能由 X 和 Y 染色体以及中肠微生物群组成。这个基因组有合理数量的未被调用的位置(也就是说,你会发现 N s 而不是 ACTGs)。萎缩按蚊的基因组仍然是支架形式。不幸的是,这是你会发现许多非模式物种。

请注意,我们将增加一点赌注。按蚊的基因组比恶性疟原虫 ?? 的基因组大一个数量级(但仍然比大多数哺乳动物小一个数量级)。

我们会使用你在 第一章**Python 以及周边软件生态中安装的 Biopython。像往常一样,这个食谱可以在本书的 Jupyter 笔记本的Chapter05/Low_Quality.py中找到,在本书的代码包中。在笔记本的开始,你可以找到两个基因组的最新位置,以及下载它们的代码。

怎么做...

请遵循以下步骤:

  1. 让我们首先列出冈比亚按蚊基因组*的染色体:

    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)染色体是参考基因组的一大部分,相当于一个染色体臂。

不要用微小按蚊来做这个;否则,由于脚手架状态,您将获得一千多个条目。

  1. 现在,让我们检查冈比亚按蚊基因组:

    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游程大小的分布。为了计算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 bps。请注意,X染色体具有最高比例的Ns位置。

  1. 现在,让我们把注意力转向萎缩按蚊的基因组。让我们数一下脚手架的数量,以及脚手架尺寸的分布:

    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个支架(相对于冈比亚按蚊基因组上的 7 个条目),其中值为7811.5(?? 的平均值)。最大的支架是 5.8 Mbp,最小的支架是 1,004 bp。尺寸的第十百分位数是1537.1,而第九十百分位数是39644.7

  1. 最后,让我们绘制支架的分数——即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)
    

前面的代码将生成下图所示的输出,其中我们根据支架大小将图分成三部分:一部分用于小于 4,800 bp 的支架,一部分用于 4800 到 540,000 bp 的支架,一部分用于更大的支架。对于小型支架,Ns的分数非常低(总是低于 3.5%);对于中型支架,差异较大(大小在 0%和 90%以上),对于最大的支架,差异较小(在 0%和 25%之间):

图 5.3-支架的 N 分数与其尺寸的函数关系

还有更多...

有时,参考基因组携带额外的信息。例如,冈比亚按蚊基因组被软屏蔽。这意味着在基因组上运行了一些程序来识别低复杂性区域(这些区域通常更难以分析)。这可以通过大写来说明:ACTG 将是高复杂度的,而 actg 将是低复杂度的。

有许多支架的参考基因组不仅仅是一个不方便的争论。例如,非常小的支架(比如 2,000 bp 以下)在使用校准器时可能会有作图问题(例如布伦斯-惠勒校准器 ( BWA )),特别是在极端情况下(大多数支架在其极端情况下都会有作图问题,但如果支架很小,这些问题在支架中的比例会大得多)。如果您正在使用这样的参考基因组进行比对,您将需要考虑在绘制小支架时忽略配对信息(假设您有配对末端读数),或者至少测量支架大小对您的比对器性能的影响。在任何情况下,总的想法是,你应该小心,因为脚手架的大小和数量会不时地露出它们丑陋的头。

对于这些基因组,仅识别出完全模糊性(N)。注意,其他基因组组装会给你一个介于总的模糊性和确定性 ( ACTG )之间的中间代码。

参见

以下是一些资源,您可以从中了解更多信息:

遍历基因组注释

拥有一个基因组序列是有趣的,但是我们想要从中提取特征,比如基因、外显子和编码序列。这种类型的注释信息在通用特征格式 ( GFF )和通用传输格式 ( GTF )文件中可用。在这个菜谱中,我们将学习如何解析和分析 GFF 文件,同时以冈比亚按蚊基因组的注释为例。

准备就绪

使用本书的代码包中提供的Chapter05/Annotations.py笔记本文件。我们将使用的 GFF 文件的最新位置可以在笔记本的顶部找到。

您需要安装gffutils:

conda install -c bioconda gffutils

现在,我们准备开始了。

怎么做...

请遵循以下步骤:

  1. 让我们首先基于我们的 GFF 文件:

    import gffutils
    import sqlite3
    try:
        db = gffutils.create_db('gambiae.gff.gz', 'ag.db')
    except sqlite3.OperationalError:
        db = gffutils.FeatureDB('ag.db')
    

    gffutils创建一个注释数据库

gffutils库创建了一个 SQLite 数据库来高效地存储注释。这里,我们将尝试使用来创建数据库,但是如果它已经存在,我们将使用现有的数据库。这一步可能很耗时。

  1. 现在,让我们列出所有可用的功能类型并进行计数:

    print(list(db.featuretypes()))
    for feat_type in db.featuretypes():
        print(feat_type, db.count_features_of_type(feat_type))
    

这些特征将包括重叠群、基因、外显子、转录本等等。注意,我们将使用gffutils包的featuretypes函数。它将返回一个生成器,但是我们将把它转换成一个列表(在这里这样做是安全的)。

  1. 让我们列出所有的序列号:

    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。然而,丑陋的事实是,你会发现处理文件非常困难。图书馆尽最大努力去适应这一点。事实上,这个库的大部分文档都与帮助你处理各种尴尬的变化有关(参考 https://pythonhosted.org/gffutils/examples.xhtml)。

使用gffutils还有一个替代方法(要么是因为你的 GFF 文件很奇怪,要么是因为你不喜欢库接口或它对 SQL 后端的依赖)。手动解析文件。如果你看看它的格式,你会发现它并不复杂。如果您只是执行一次性操作,那么手动解析可能就足够了。当然,从长远来看,一次性操作往往没那么好。

另外,请注意注释的质量往往变化很大。随着质量的提高,复杂性也在增加。只需查看人工注释中的一个例子。你可以预期,随着时间的推移,随着我们对有机体知识的发展,注释的质量和复杂性将会增加。

参见

以下是一些资源,您可以从中了解更多信息:

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

在这份食谱中,我们将学习如何在注释文件的帮助下提取基因序列,以获得其相对于参考 FASTA 的坐标。我们将使用冈比亚按蚊的基因组,以及它的注释文件(按照前两个食谱)。首先,我们将提取电压门控钠通道 ( VGSC )基因,该基因涉及对杀虫剂的抗性。

准备就绪

如果你已经遵循了前两个食谱,你将准备好了。如果没有,下载冈比亚按蚊 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 中检索到的,vector base 是一个疾病媒介基因组学的在线数据库。对于其他特定的情况,你需要知道你的基因的 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. 让我们创建一个函数来为一个列表CDSs :

    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 号染色体上的 1,234 位可能指的是与第 38 号染色体上的 1,234 位不同的 SNP。对于人类数据,你可能会在 build 36 上找到很多芯片,在 build 37 上找到大量全基因组序列,而最近的人类组装是 build 38。以我们的按蚊为例,你会有第 3 和第 4 个版本。大多数物种都会这样。所以,要注意!

Python 中的 0 索引数组与 1 索引基因组数据库之间也存在问题。尽管如此,请注意一些基因组数据库也可能是 0 索引的。

还有两个混淆的来源:转录本和基因选择,就像在更丰富的注释数据库中一样。这里,您将有几个可供选择的抄本(如果您想查看一个丰富到令人困惑的数据库,请参考人工注释数据库)。此外,与编码序列相比,标记有exon的字段将包含更多信息。为此,您将需要 CDS 字段。

最后,还有一个链的问题,在这里你要根据反向互补来翻译。

参见

以下是一些资源,您可以从中了解更多信息:

使用 Ensembl REST API 查找直系同源词

在这份食谱中,我们将学习如何寻找某个基因的直系同源物。这个简单的食谱不仅将介绍正字法检索,还将介绍如何在 web 上使用 REST APIs 来访问生物学数据。最后,但同样重要的是,它将介绍如何使用编程 API 访问 Ensembl 数据库。

在我们的例子中,我们将试图找到人类乳糖酶 ( LCT )基因在horse基因组上的任何直系同源物。

准备就绪

这个食谱不需要任何预先下载的数据,但是由于我们使用网络应用编程接口,将需要互联网接入。可以传输的数据量将会受到限制。

我们还将利用requests库来访问 Ensembl。request API 是一个易于使用的 web 请求包装器。当然,您可以使用标准的 Python 库,但是这些库要麻烦得多。

和往常一样,你可以在Chapter05/Orthology.py笔记本文件中找到这个内容。

怎么做...

请遵循以下步骤:

  1. 我们将首先创建一个支持函数来执行一个 web 请求:

    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'])
    

注意,这将为 REST 请求构建一个以前缀http://rest.ensembl.org/info/species开头的 URL。顺便说一下,前面的链接在你的浏览器上不起作用;它应该只通过 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 数据库负责管理人类基因名称并维护我们的 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)
    

你会发现不同种类的数据库,比如脊椎动物基因组注释 ( 织女星)项目、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基因组的直系同源物。但是,此代码允许您检查所有可用的正字法。

你会得到关于一个直向同源物的相当多的信息,比如直向同源物的分类级别(boreoutheria——胎盘哺乳动物是人类和马之间最接近的系统发育级别),直向同源物的 Ensembl ID,dN/dS 比率(非同义突变到同义突变),以及序列间差异的雪茄串(参考上一章, 第三章下一代测序)。默认情况下,您还将获得直向同源序列的对齐,但是我已经将其移除以清除输出。

  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 数据库在 第三章下一代测序中有所介绍。

以编程方式与 Ensembl 交互的另一个完全不同的策略是下载原始表,并将它们注入本地 MySQL 数据库。请注意,这本身就是一项艰巨的任务(您可能只想加载非常小的一部分表)。但是,如果您打算大量使用,您可能需要考虑创建数据库的一部分的本地版本。如果是这种情况,您可能需要重新考虑 UCSC 的替代方案,因为从本地数据库的角度来看,它和 Ensembl 一样好。

从 Ensembl 中检索基因本体信息

在这个菜谱中,您将通过查询 Ensembl REST API 再次学习如何使用基因本体信息。基因本体是受控的词汇,用于注释基因和基因产物。这些以概念树的形式提供(更一般的概念位于层次结构的顶端)。基因本体论有三个领域:细胞成分、分子功能和生物过程。

准备就绪

和前面的方法一样,我们不需要任何预先下载的数据,但是因为我们使用 web APIs,所以需要互联网访问。将要传输的数据量将是有限的。

和往常一样,你可以在Chapter05/Gene_Ontology.py笔记本文件中找到这个内容。我们将使用do_request函数,它是在前面的配方的步骤 1 中定义的(使用 Ensembl REST API 查找直系同源)。为了绘制围棋树,我们将使用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. 让我们专注于lactase activity的分子功能,并检索关于它的更详细的信息(下面的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']))
    

我们打印lactase activity记录(当前是 GO 树分子函数的一个节点)并检索一个潜在父节点列表。此记录有一个单亲。我们检索它并打印孩子的数量。

  1. 让我们检索lactase activity分子函数的所有通用术语(同样,父代和所有其他祖先):

    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关系来检索ancestor列表(请参考中的 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. 最后,我们将打印出lactase activity术语的关系树。为此,我们将使用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')
    

下面的输出显示了lactase activity术语的本体树:

Figure 5.4 – An ontology tree for the “lactase activity” term (the terms at the top are more general); the top of the tree is molecular_function; for all ancestral nodes, the number of extra offspring is also noted (or enumerated, if less than three)

图 5.4–术语“乳糖酶活性”的本体树(顶部的术语更通用);树的顶端是分子 _ 功能;对于所有的祖先节点,还会记录额外后代的数量(如果少于三个,则进行枚举)

还有更多...

如果你对基因本体论感兴趣,你的主要停靠站将是geneontology.org,在那里你将找到更多关于这个主题的信息。除了molecular_function,基因本体还有一个生物过程和一个细胞成分。在我们的食谱中,我们遵循了等级关系是一个,但是其他的确实部分存在。例如,“线粒体核糖体”(GO:0005761)是一种细胞成分,是“线粒体基质”的一部分(参见amigo . gene ontology . 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/bio informatics _ pop gen。

在本章中,我们将介绍以下配方:

  • 使用 PLINK 管理数据集
  • 利用 sgkit 和 xarray 进行群体遗传学分析
  • 使用 sgkit 探索数据集
  • 分析人口结构
  • 执行 PCA
  • 混合研究种群结构

首先,让我们从讨论文件格式问题开始,然后继续讨论有趣的数据分析。

使用 PLINK 管理数据集

这里,我们将使用 PLINK 管理我们的数据集。我们将创建我们的主数据集(来自 HapMap 项目)的子集,这些子集适合于在下面的食谱中进行分析。

警告

请注意,PLINK 和任何类似的程序都不是为它们的文件格式开发的。可能没有为群体遗传学数据创建默认文件标准的目标。在这个领域中,您需要准备好从一种格式到另一种格式的转换(对于这一点,Python 是非常合适的),因为您将使用的每个应用程序可能都有自己古怪的需求。从这份食谱中学到的最重要的一点是,使用的不是格式,尽管这些是相关的,而是一种“文件转换心态”。除此之外,这个方法中的一些步骤也传达了真正的分析技术,你可能想考虑使用,例如,子采样或连锁不平衡- ( LD- )修剪。

准备就绪

在本章中,我们将使用来自国际单体型图项目的数据。你可能还记得,我们在第三章 、下一代测序中使用了来自 1000 个基因组项目的数据,并且 HapMap 项目在许多方面是 1000 个基因组项目的前身;基因分型取代了全基因组测序。HapMap 项目的大多数样本在 1,000 个基因组项目中使用,所以如果你已经阅读了 第三章下一代测序中的配方,你将已经对数据集(包括可用群体)有了一个概念。我就不多介绍数据集了,不过你可以参考 第三章下一代测序,还有课程的 HapMap 网站(www . genome . gov/10001688/international-hap map-project)了解更多信息。请记住,我们有全球不同人群中许多个体的基因分型数据。我们将用这些人群的首字母缩写来称呼他们。以下是摘自www . Sanger . AC . uk/resources/downloads/human/hapmap 3 . XHTML的列表:

| **缩写** | **人口** | | 科学工作者协会(Association of Scientific Workers) | 美国西南部的非洲血统 | | CEU | 犹他州居民与北欧和西欧血统从 CEPH 收集 | | CHB | 中国北京的汉族人 | | 冠心病 | 科罗拉多州丹佛市的华人 | | GIH | 德克萨斯州休斯顿的古吉拉特印第安人 | | 发动机尾喷管温度(jet pipe temperature) | 日本东京的日本人 | | LWK | 肯尼亚韦布耶的卢赫亚 | | MXL | 加利福尼亚洛杉矶的墨西哥血统 | | MKK | 肯尼亚金亚瓦的马赛人 | | tonspersquareinch 每平方英寸吨数 | 意大利的托斯卡尼 | | YRI | 尼日利亚伊巴丹的约鲁巴语 |

表 6.1 -基因组计划中的人群

注意

我们将使用来自 HapMap 项目的数据,该项目实际上已被 1000 基因组项目所取代。为了用 Python 教授群体遗传学编程技术,HapMap 项目数据集比 1,000 基因组项目更易于管理,因为数据要小得多。HapMap 样本是 1,000 个基因组样本的子集。如果你从事人类群体遗传学的研究,强烈建议你使用 1000 个基因组项目作为基础数据集。

这将需要相当大的下载量(大约 1 GB ),并且必须解压缩。确保您有大约 20 GB 的磁盘空间用于本章。这些文件可以在FTP . NCBI . NLM . NIH . gov/hap map/genetics/hap map 3 _ 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 文件;图谱文件包含基因组中标记位置的信息,而 PED 文件包含每个个体的实际标记,以及一些谱系信息。我们还下载了一个包含每个人信息的元数据文件。看看这些文件,熟悉一下。像往常一样,这也可以在Chapter06/Data_Formats.py笔记本文件中找到,在那里一切都已经处理好了。

最后,这个食谱的大部分将大量使用 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%的子样本数据集(前缀为hapmap10_autohapmap1_auto)创建仅自动染色体 PLINK 文件。

  4. 让我们创建一些没有后代的数据集。大多数群体遗传分析都需要这些,这在一定程度上需要不相关的个体:

    os.system('plink2 --pedmap hapmap10_auto --filter-founders --out hapmap10_auto_noofs --export ped')
    

注意

这一步代表了大多数群体遗传分析需要样本在一定程度上不相关的事实。显然,当我们知道一些后代在 HapMap 中时,我们删除它们。

但是,请注意,对于您的数据集,您应该比这更精确。例如,运行plink --genome或使用另一个程序来检测相关的个人。这里的基本点是,你必须付出一些努力来检测你的样本中的相关个体;这不是一项微不足道的任务。

  1. 我们还将生成一个 LD-pruned 数据集,如许多 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-pruned,则保留该列表。这使用了一个50 SNPs 的滑动窗口,每次前进10 SNPs,切割值为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-pruning 对于人类分析来说有些标准,但是一定要检查参数,特别是如果您使用非人类数据的话。

我们下载的 HapMap 文件基于参考基因组的旧版本(build 36)。如前一章所述, 第五章使用基因组,如果您打算使用这个文件进行更多的分析,请务必使用 build 36 中的注释。

该配方为后续配方奠定了基础,其结果将被广泛使用。

参见

利用 sgkit 对 xarray 进行群体遗传学分析

Sgkit 是做群体遗传学分析的最先进的 Python 库。这是一个现代的实现,利用了 Python 中几乎所有的基础数据科学库。当我说几乎所有的时候,我并没有夸大其词;它使用 NumPy,pandas,xarray,Zarr 和 Dask。NumPy 和熊猫是在 第二章 中介绍的。这里,我们将介绍 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-如果在笔记本中-将生成以下表示:

Figure 6.1 - An overview of the xarray data loaded by sgkit for our PLINK file

图 6.1-sgkit 为我们的 PLINK 文件加载的 xarray 数据的概述

data是一个 xarray 数据集。xarray 数据集本质上是一个字典,其中每个值都是一个 Dask 数组。出于我们的目的,您可以假设它是一个 NumPy 数组。在这种情况下,我们可以看到我们有 1198 样本的 56241 变体。我们每个变体有 2 个等位基因,倍性为 2

在笔记本中,我们可以展开每个条目。在我们的例子中,我们扩展了call_genotype。这是一个三维数组,有variantssamplesploidy个维度。数组的类型是int8。在此之后,我们可以找到一些与条目、mixed_ploidy和注释相关的元数据。最后,您对 Dask 实现进行了总结。数组列显示了数组的大小和形状的详细信息。对于列,参见 第十一章——但是你现在可以放心地忽略它。

  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方法。

这个调用并不麻烦——我们的代码工作的原因是我们的数据集足够小,可以放入内存,这对我们的教学例子来说非常好。但是如果你有一个非常大的数据集,前面的代码就太天真了。还是那句话, 第十一章 会帮你搞定。目前,这种简单是教学用的。

  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 IDrs11510103

  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 教程。关于哈维还有很多要说的——一定要看看 https://docs.xarray.dev/。值得重申的是,xarray 依赖于大量的 Python 数据科学库,我们暂时忽略了 Dask。

使用 sgkit 探索数据集

在这个菜谱中,我们将对我们生成的数据集之一进行初步探索性分析。现在我们对 xarray 有了一些基本的了解,我们可以实际尝试做一些数据分析。在这个配方中,我们将忽略人口结构,这个问题我们将在下一个配方中再讨论。

准备就绪

您需要运行第一个配方,并且应该有可用的hapmap10_auto_noofs_ld文件。有一个笔记本文件里有这个食谱,叫做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
    

输出如下所示:

Figure 6.2 - The variant statistics provided by sgkit’s variant_stats

图 6.2-SG kit 的 variant_stats 提供的变量统计数据

  1. 现在让我们看看统计数据,variant_call_rate :

    variant_stats.variant_call_rate.to_series().describe()
    

这里要解开的比看起来的要多。最基本的部分是to_series()调用。Sgkit 将向您返回一个熊猫系列——请记住,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 提供了大量现成的样本统计信息:

Figure 6.3 - The sample statistics obtained by calling sample_stats

图 6.3 -通过调用 sample_stats 获得的样本统计数据

  1. 现在我们将让来看一下样本看涨期权价格:

    sample_stats.sample_call_rate.to_series().hist()
    

这一次,我们绘制了样本调用率的直方图。同样,sgkit 通过利用 Pandas 免费获得此功能:

Figure 6.4 - The histogram of sample call rates

图 6.4 -样本呼叫率直方图

还有更多...

事实是,对于群体基因分析来说,没有什么能打败 R;绝对鼓励你去看看现有的群体遗传学 R 库。别忘了还有一个 Python-R 桥,在 第一章Python 以及周边的软件生态中讨论过。

如果在更大的数据集上进行,这里介绍的大部分分析在计算上将是昂贵的。事实上,sgkit 已经准备好处理这个问题,因为它利用了 Dask。在这个阶段引入 Dask 会太复杂,但是对于大型数据集, 第十一章 将讨论解决这些问题的方法。

参见

  • 统计遗传学的 R 包列表可在cran.r-project.org/web/views/Genetics.xhtml获得。
  • 如果你需要了解更多关于群体遗传学的知识,我推荐《群体遗传学原理》这本书,作者是的丹尼尔·l·哈特尔和的安德鲁·g·克拉克。**

分析人口结构

之前,我们用 sgkit 引入了数据分析,忽略了群体结构。大多数数据集,包括我们正在使用的数据集,实际上都有一个群体结构。Sgkit 提供了用群体结构分析基因组数据集的功能,这就是我们在这里要研究的内容。

准备就绪

您需要运行第一个配方,并且应该下载我们生成的hapmap10_auto_noofs_ld数据和原始群体元数据relationships_w_pops_041510.txt文件。有一个笔记本文件,里面有06_PopGen/Pop_Stats.py的配方。

怎么做...

看看下面的步骤:

  1. 首先,让我们用 sgkit:

    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')
    

    加载 PLINK 数据

  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')
    

记住 SG kit 中的每个样本在一个数组中都有一个位置。因此,我们必须创建一个数组,其中的每个元素都指向样本中的特定人群或群体。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】

我们为每个种群创建 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 图中。结果如下:

Figure 6.5 - A MAF histogram for the YRI and JPT populations

图 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 :

    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()
    

    人口之间的fst

我们从stat_FST中获取由sel函数返回的成对结果,用它来比较和创建熊猫系列。请注意,我们可以通过名称来引用群体,因为我们在上一步中已经准备好了坐标。

  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 的平均值:

Figure 6.6 - The average pairwise FST across the 11 populations in the HapMap project for all autosomes

图 6.6-hap map 项目中所有常染色体的 11 个群体的平均成对 FST

参见

执行 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-修剪标记;我们不会冒险在这里使用后代,因为它可能会使结果有偏差。我们将使用带有等位基因12的重新编码的 PLINK 文件,因为这使得用 SmartPCA 和 scikit-learn 进行处理更容易。

我有一个简单的库来帮助一些基因组处理。你可以在 https://github.com/tiagoantao/pygenomics 找到这个代码。您可以使用以下命令安装它:

pip install pygenomics

对于这个食谱,你将需要下载 eigen soft(www.hsph.harvard.edu/alkes-price/software/),其中包括我们将使用的 SmartPCA 应用程序。

Chapter06/PCA.py配方中有一个笔记本文件,但是您仍然需要运行第一个配方。

怎么做...

看看下面的步骤:

  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)
    

这将产生下图。我们将提供绘图功能和从元数据中检索的人口信息,这允许您用不同的颜色绘制每个人口。结果与已发表的结果非常相似;我们会发现四组。大多数亚洲人口位于顶部,非洲人口位于右侧,欧洲人口位于底部。另外两个混合种群( GIH墨西哥)位于中间:

Figure 6.7 - PC 1 and PC 2 of the HapMap data, as produced by SmartPCA

图 6.7-smart PCA 产生的 HapMap 数据的 PC 1 和 PC 2

注意

注意,PCA 图可以在运行的任何轴上对称,因为信号无关紧要。重要的是聚类应该是相同的,并且个体(和这些聚类)之间的距离应该是相似的。

还有更多...

这里一个有趣的问题是你应该使用哪种方法——smart PCA 还是 scikit-learn,我们会在 第十章 中用到。结果是相似的,所以如果您正在执行自己的分析,您可以自由选择。然而,如果你在科学杂志上发表你的结果,SmartPCA 可能是一个更安全的选择,因为它是基于遗传学领域的软件发表的;评论家可能会喜欢这个。

参见

用混合物研究种群结构

人口遗传学中一个典型的分析是由程序结构(web.stanford.edu/group/pritchardlab/structure.xhtml)推广的,用于研究人口结构。这种类型的软件用于推断存在多少种群(或多少祖先种群产生了当前种群),并识别潜在的移民和混合个体。这个结构是在相当一段时间以前开发的,当时进行基因分型的标记要少得多(当时,这主要是少数微卫星),并且开发了更快的版本,包括来自同一实验室的一个叫做fastStructure(rajanil.github.io/fastStructure/)。在这里,我们将使用 Python 与加州大学洛杉矶分校开发的相同类型的程序接口,该程序被称为混合程序(dalexander.github.io/admixture/download.xhtml)。

准备就绪

为了使用hapmap10_auto_noofs_ld二进制 PLINK 文件,您需要运行第一个配方。同样,我们将使用 10%的被 LD 修剪过的没有后代的常染色体的二次抽样。

正如在前面的食谱中,你将使用的pygenomics库来帮助;你可以在 https://github.com/tiagoantao/pygenomics 找到这些代码文件。您可以使用以下命令安装它:

pip install pygenomics

理论上,对于这个食谱,你需要下载混合物(www.genetics.ucla.edu/software/admixture/)。然而,在这种情况下,我将在我们将使用的 HapMap 数据上提供运行混合物的输出,因为运行混合物需要很多时间。您可以使用可用的结果或自己运行混合物。在Chapter06/Admixture.py配方中有一个笔记本文件,但是你仍然需要先运行配方。

怎么做...

看看下面的步骤:

  1. 首先,让我们定义我们感兴趣的k(一些祖先种群)范围,如下:

    k_range = range(2, 10)  # 2..9
    
  2. 让我们为所有的k运行混合(或者,您可以跳过这一步,使用提供的示例数据):

    for k in k_range:
        os.system('admixture --cv=10 hapmap10_auto_noofs_ld.bed %d > admix.%d' % (k, k))
    

注意

这是运行混合物最糟糕的方式,如果你这样做的话,可能要花 3 个多小时。这是因为它将按顺序运行从29的所有k。有两件事可以加快速度:使用混合提供的多线程选项(-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)
    

下面的图绘制了2的 a K9之间的CV,越低越好。从这张图中可以清楚地看出,我们也许应该运行更多的K(实际上,我们有 11 个种群;如果不是更多,我们应该至少运行到 11),但由于计算成本,我们停在9

关于是否存在“最佳”这种东西,这将是一场非常技术性的辩论。现代科学文献表明,可能不存在一个“最佳”K;这些结果值得做一些解释。我认为在你继续解释K结果之前,意识到这一点很重要:

Figure 6.8 - The error by 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的数量会有多少组件。这里,我们将为我们研究的所有k加载Q文件,并将它们存储在一个字典中,其中个人 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/py genomics/blob/master/genomics/pop gen/mixed/_ _ 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(我们在这里选择了7中的一个K)。

请注意,所有的K都值得进行一些解释(例如,K = 2将非洲人口与其他人分开,K = 3将欧洲人口分开,并显示了 GIH墨西哥的混合):

Figure 6.9 - A stacked admixture plot (between K of 2 and 9) for the HapMap example

图 6.9-hap map 示例的堆积混合图(K 在 2 和 9 之间)

还有更多...

不幸的是,你不能运行一个混合物的实例来得到一个结果。最佳实践是实际运行 100 个实例,并获得具有最佳对数似然性的实例(在混合输出中报告)。显然,我不能要求你为这个食谱的 7 个不同的K中的每一个运行 100 个实例(我们正在谈论两周的计算),但是如果你想要有可发布的结果,你可能需要来执行这个。运行这个需要一个集群(或者至少是一个非常好的机器)。您可以使用 Python 检查输出并选择最佳对数似然。在为每个K选择了具有最佳对数似然的结果后,您可以很容易地应用这个方法来绘制输出。

七、系统发育学

系统发育学是分子测序的应用,用于研究生物之间的进化关系。说明这一过程的典型方法是使用系统进化树。从基因组数据计算这些树是一个活跃的研究领域,具有许多现实世界的应用。

在本书中,我们将把提到的实用方法提升到一个新的水平:这里的大多数食谱都受到了最近一项关于埃博拉病毒的研究的启发,该研究研究了最近在非洲爆发的埃博拉病毒。这项研究名为基因组监测,阐明了 2014 年埃博拉病毒爆发期间的起源和传播,作者 Gire 等人,发表在科学上。在pubmed.ncbi.nlm.nih.gov/25214632/有售。在这里,我们将尝试遵循类似的方法,以达到类似的论文结果。

在这一章中,我们将使用 DendroPy(一个系统发育库)和 Biopython。Docker 映像包括所有必要的软件。

在本章中,我们将介绍以下配方:

  • 为系统发育分析准备数据集
  • 对齐遗传和基因组数据
  • 比较序列
  • 重建系统进化树
  • 递归地玩树
  • 可视化系统发育数据

准备用于系统发育分析的数据集

在这个菜谱中,我们将下载并准备用于我们分析的数据集。该数据集包含埃博拉病毒的完整基因组。我们将使用 DendroPy 来下载和准备数据。

准备就绪

我们将从 GenBank 下载完整的基因组;这些基因组是从各种埃博拉疫情中收集的,包括 2014 年爆发的几次疫情。请注意,有几种病毒会导致埃博拉病毒病;2014 年疫情涉及的物种(EBOV 病毒,正式名称为扎伊尔埃博拉病毒)是最常见的,但这种疾病是由埃博拉病毒属的更多物种引起的;另外四种也以序列形式提供。你可以在en.wikipedia.org/wiki/Ebolavirus了解更多。

如果您已经阅读了前面的章节,您可能会对这里涉及的潜在数据大小感到恐慌;这根本不是问题,因为这些病毒的基因组大小都在 19 kbp 左右。所以,我们大约 100 个基因组实际上很轻。

为了进行这个分析,我们需要安装dendropy。如果您正在使用 Anaconda,请执行以下操作:

conda install –c bioconda dendropy

像往常一样,这个信息可以在相应的 Jupyter 笔记本文件中找到,该文件可以在Chapter07/Exploration.py找到。

怎么做...

看看下面的步骤:

  1. 首先,让我们从使用树复制指定我们的数据源开始,如下:

    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 文件,这些文件要么包括所有基因组,要么只包括 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树类,并计算分类群的数量。然后,我们提取每个序列的所有氨基酸(我们排除了由-确定的缺口)来计算长度并报告最小、平均和最大大小。关于 API 的更多细节,请看一下树图文档。

  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 森林病毒=TAFV,苏丹病毒=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,在 http://etetoolkit.org/ ?? 有售。

参见

比对遗传和基因组数据

在我们能够进行任何系统发育分析之前,我们需要比对我们的遗传和基因组数据。这里,我们将使用 maft(【http://mafft.cbrc.jp/alignment/software/】)对进行基因组分析。基因分析将使用肌肉进行(www.drive5.com/muscle/)。

准备就绪

要执行基因组比对,您需要安装 MAFFT。此外,为了进行基因比对,将使用肌肉。此外,我们将使用 trimAl(trimal.cgenomics.org/)以自动方式去除虚假序列和排列不良的区域。所有包装均可从 Bioconda 获得:

conda install –c bioconda mafft trimal muscle=3.8

通常,这些信息可以在Chapter07/Alignment.py的相应 Jupyter 笔记本文件中找到。您需要事先运行上一个笔记本,因为它将生成此处需要的文件。在本章中,我们将使用 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)、田岛的 D ( taj_d)和沃特森的θ(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. 首先,让我们看看输出,然后我们将解释如何构建它:

Figure 7.1 – A DataFrame for the virus dataset

图 7.1–病毒数据集的数据框架

我使用了一个pandas DataFrame 来打印结果,因为它真的是为处理这样的操作而定制的。我们将用一个 NumPy 多维数组初始化我们的数据框架,该数组有四行(基因)和四个统计数据乘以两个物种。

统计数据,如分离位点的数量、核苷酸多样性、Tajima’s D 和 Watterson’s theta,由 DendroPy 计算。注意数组中单个数据点的位置(坐标计算)。

看最后一行:如果你在 Jupyter 中,只需在末尾加上df就能显示数据帧和单元格输出。如果你不在笔记本里,就用print(df)(你也可以在笔记本里执行这个,但是看起来不会那么好看)。

  1. 现在,让我们提取类似的信息,但是是全基因组的,而不仅仅是全基因的。在这种情况下,我们将使用两次 EBOV 暴发(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 年 EBOV 爆发的数据集很大,我们对其进行了二次抽样,只有 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,一个基于最大似然推理大型系统树的程序,你可以在 http://sco.h-its.org/exelixis/software.xhtml查看。Bioconda 也包括它,但它被命名为raxml。注意,这个二进制文件叫做raxmlHPC。您可以执行以下命令来安装它:

conda install –c bioconda raxml

前面的代码很简单,但是执行起来需要时间,因为它将调用 RAxML(这是计算密集型的)。如果您选择使用树型接口,它也可能会占用大量内存。我们将与 RAxML、DendroPy 和 Biopython 交互,让您选择使用哪个接口;DendroPy 为您提供了一种访问结果的简单方法,而 Biopython 对内存的要求较低。尽管在本章的后面有可视化的方法,我们还是会在这里绘制一个生成的树。

通常,这些信息可在Chapter07/Reconstruction.py的相应笔记本中找到。您将需要上一个食谱的输出来完成这个。

怎么做...

看看下面的步骤:

  1. 对于 DendroPy,我们将首先加载数据,然后重建 genus 数据集,如下所示:

    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 错误。

我们将使用 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 改变了序列的名称,例如,增加了它们的大小。这很容易解决;我们将在可视化系统发育数据配方中处理这个问题:

Figure 7.2 – The phylogenetic tree that we generated with RAxML for all Ebola viruses

图 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()
    

与 Biopython 相比,DendroPy 有一个更加声明性的接口,所以你可以处理一些额外的事情。您应该指定种子(如果您不这样做,Biopython 将设置一个固定的默认值 10,000)和工作目录。对于 RAxML,工作目录规范需要绝对路径。

  1. 让我们来看看 Biopython 运行的结果。虽然对于 DendroPy 和 Biopython 来说,RAxML 输出是相同的(除了随机性),但是 DendroPy 抽象了一些东西。使用 Biopython,您需要自己处理结果。您也可以使用树状视图来执行此操作;但是,在这种情况下,它是可选的:

    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/clad istics 5 . 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 的节点表示有一个 level 方法(用于比较),但这里的重点是引入一个递归算法,所以无论如何我们都会实现。

注意函数是如何工作的;用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加上它整个后代的最大高度。

注意,我们使用 lambda 函数上的 map 来获取当前节点的所有子节点的高度。然后,我们选择最大值(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 )中。这是因为当你在两个极端操作时,它实现了一个有效的数据结构。

该算法从将根节点放入队列开始。当队列不为空时,我们将把节点放在前面。如果它是一个内部节点,我们将把它的所有子节点放入队列中。

我们将重复前面的步骤,直到队列为空。我鼓励你拿起笔和纸,通过执行下图所示的示例来看看这是如何工作的。代码很小,但却很重要:

Figure 7.3 – Visiting a tree; the first number indicates the order in which that node is visited traversing depth-first, while the second assumes breadth-first

图 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 的情况具有相同的物种和爆发年份,我们将删除所有子节点、叶子和内部子树节点。

如果它们不属于同一个物种,我们会递归下去,直到发生这种情况。最坏的情况是,当你已经在一个叶节点时,该算法简单地解析为当前节点的种类。

还有更多...

有大量关于树和数据结构的计算机科学文献;如果你想了解更多,维基百科在 http://en.wikipedia.org/wiki/Tree_(data_structure) 提供了很棒的介绍。

注意,作为 Python 方言,不鼓励使用lambda函数和map;你可以在 http://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 表示如下图所示。这里,我们不会打印完整的版本,因为它需要几页纸。但是如果您运行前面的代码,您将能够看到它实际上是非常可读的:

Figure 7.4 – The ASCII representation of a simplified Ebola virus dataset

图 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 的长度,以避免混乱。这样做的结果如下图所示:

Figure 7.5 – A matplotlib-based version of the simplified dataset with branch lengths added

图 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 爆发年份)获得一种颜色。如果它是一个内部节点,并且它下面的所有后代节点都是同一物种,那么它将获得该物种的颜色;如果在那之后有几个物种,它将被涂成灰色。其实颜色功能是可以改的,以后也会改。仅使用边缘颜色(标签将以黑色打印)。

请注意,就清晰的视觉外观而言,梯形化(在前面的树型方法中执行)非常有帮助。

我们也深抄属树来着色一份拷贝;记住前面的方法,一些树遍历函数可以改变状态,在这种情况下,我们希望保留一个没有任何着色的版本。

注意使用 lambda 函数来清除 trimAl 更改的名称,如下图所示:

Figure 7.6 – A ladderized and colored phylogenetic tree with the complete Ebola virus dataset

图 7.6-带有完整埃博拉病毒数据集的分层彩色系统进化树

还有更多...

树和图形可视化是一个复杂的主题;可以说,在这里,树的可视化是严格的,但远不美观。DendroPy 的另一个选择是 ETE(etetoolkit.org/),它有更多的可视化特性。绘制树和图的一般备选方案有胞景(cytoscape.org/)和格菲(gephi.github.io/)。如果你想了解更多关于渲染树和图的算法,可以查看 http://en.wikipedia.org/wiki/Graph_drawing 的维基百科页面,了解这个有趣话题的介绍。

但是,注意不要用形式来换取实质。例如,这本书的前一版使用图形渲染库对系统进化树进行了漂亮的渲染。虽然这显然是那一章中最美丽的图像,但它在分支长度方面有误导性。

八、使用蛋白质数据库

蛋白质组学是对蛋白质的研究,包括它们的功能和结构。该领域的主要目标之一是表征蛋白质的三维结构。蛋白质组学领域最广为人知的计算资源之一是蛋白质数据库 ( PDB ),这是一个拥有大生物分子结构数据的存储库。当然,许多数据库反而关注蛋白质一级结构;这些有点类似于我们在 第二章了解 NumPy、pandas、Arrow 和 Matplotlib 中看到的基因组数据库。

在本章中,我们将主要关注处理来自 PDB 的数据。我们将看看如何解析 PDB 文件,执行一些几何计算,并可视化分子。我们将使用旧的 PDB 文件格式,因为从概念上讲,它允许您在稳定的环境中执行大多数必要的操作。话虽如此,预定取代 PDB 格式的较新的 mmCIF 也将出现在用 Biopython 方法解析 mmCIF 文件的中。我们将使用 Biopython 并引入 PyMOL 进行可视化。我们不会在这里讨论分子对接,因为这可能更适合一本关于化学信息学的书。

在本章中,我们将使用一个蛋白质的经典例子:肿瘤蛋白 p53,一种参与细胞周期调节(例如,凋亡)的蛋白质。这种蛋白质与癌症高度相关。网上有大量关于这种蛋白质的信息。

让我们从你现在应该更熟悉的东西开始:访问数据库,特别是蛋白质的一级结构(如氨基酸序列)。

在本章中,我们将介绍以下配方:

  • 在多个数据库中查找蛋白质
  • 介绍生物。物理数据库
  • 从 PDB 文件中提取更多信息
  • 在 PDB 文件上计算分子距离
  • 执行几何运算
  • 使用 PyMOL 制作动画
  • 用 Biopython 解析 mmCIF 文件

在多个数据库中寻找一种蛋白质

在我们开始执行更多的结构生物学之前,我们将看看如何访问现有的蛋白质组数据库,如 UniProt。我们将在 UniProt 中查询我们感兴趣的基因 TP53 ,并从那里获取它。

准备就绪

为了访问数据,我们将使用 Biopython 和 REST API(我们在第五章 、使用基因组中使用了类似的方法)和requests库来访问 web APIs。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来简化制表符分隔的列表的处理和漂亮的打印。笔记本的节略输出如下:

Figure 8.1 - An abridged list of species for which there is a TP53 protein

图 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. 深入查看前面的记录会发现很多非常有趣的信息,特别是关于特征、基因本体 ( GO )和数据库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 条目,以及三个例子。目前,仅这种蛋白质就有 1509 个特征、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

介绍简历。物理数据库

在这里,我们将介绍 Biopython 的PDB模块,用于处理 PDB。我们将使用代表部分p53蛋白质的三个模型。你可以在 http://www.rcsb.org/pdb/101/motm.do?momID=31阅读更多关于和p53的文件。

准备就绪

您应该已经知道模型、链、剩余和原子对象的基本数据模型。在bio python . org/wiki/The _ bio python _ Structural _ bio informatics _ FAQ可以找到关于 Biopython 的结构生物信息学 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. 让我们在1TUP模型中得到除水以外的所有非标准残留物(HETATM),如下面的代码所示:

    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笔记本文件里找到这个内容。

怎么做...

我们将按照以下步骤开始:

  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/ZZ/Y ):

Figure 8.2 - The spatial distribution of the protein chains – the main figure is a 3D plot and the left subplots are planar views (X/Y, X/Z, and Z/Y)

图 8.2 -蛋白质链的空间分布-主图是一个 3D 图,左边的子图是平面图(X/Y、X/Z 和 Z/Y)

在 PDB 文件上计算分子距离

在这里,我们将在1TUP模型的中找到更接近三个锌的原子。我们将考虑到这些锌的几个距离。我们将借此机会讨论算法的性能。

准备就绪

你可以在Chapter08/Distance.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. 我们现在将得到我们的 zincs,稍后我们将进行比较:

    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 ng strm:

    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]

我们只有三个 zincs,所以计算量大大减少了。然而,想象我们有更多的原子,或者我们正在对集合中的所有原子进行成对比较(记住,在成对的情况下,比较的数量随着原子数量的二次方增长)。虽然我们的案例很小,但是预测用例并不难,而更多的比较会花费很多时间。我们将很快回到这个话题。

  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 次,然后以毫秒为单位打印结果。我们将该函数作为一个字符串传递,然后传递另一个带有必要导入的字符串,以使该函数工作。在笔记本上,你可能意识到了%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函数返回列表中通过验收标准函数的所有原子的质量。这里,默认的验收标准包括不成为水残留物。

然后我们计算所有链的质量。我们有三种版本:只有氨基酸、锌和水。在这个模型中,锌只检测每个链上的一个原子。输出如下所示:

Figure 8.3 - The mass for all protein chains

图 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)
    

结果是这里显示的:

Figure 8.4 - The center of mass and the geometric center of each protein chain

图 8.4 -每个蛋白质链的质心和几何中心

还有更多

虽然这不是一本基于蛋白质结构测定技术的书,但重要的是要记住 X 射线结晶学方法不能检测氢,所以计算残留物的质量可能是基于非常不准确的模型;更多信息请参考www . umass . edu/microbio/chime/PE _ beta/PE/prot expl/help _ hyd . htm

使用 PyMOL 制作动画

这里,我们将制作一个 p53 1TUP模型的视频。为此,我们将使用 PyMOL 可视化库。我们将通过围绕 p53 1TUP模型移动然后放大来开始我们的动画;当我们放大时,我们会改变渲染策略,以便您可以更深入地看到模型。你可以找到你将在odysee.com/@Python:8/protein_video:8制作的视频版本。

准备就绪

这个食谱将以 Python 脚本的形式呈现,而不是以笔记本的形式。这主要是因为输出不是交互式的,而是一组需要进一步后期处理的图像文件。

你需要安装 PyMOL(【http://www.pymol.org】)。在 Debian、Ubuntu 或 Linux 上,可以使用apt-get install pymol命令。如果你使用 Conda,我建议不要使用它,因为依赖性很容易解决——此外,你将安装一个需要许可证的 30 天试用版,而上面的版本是完全开源的。如果你没有使用 Debian 或 Linux,我建议你安装适用于你的操作系统的开源版本。

PyMOL 与其说是一个 Python 库,不如说是一个交互式程序,所以我强烈建议您在继续学习之前先尝试一下。这会很有趣的!这个菜谱的代码作为脚本可以在 GitHub 仓库中找到,还有本章的笔记本文件,在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 界面(带有 GUI)的情况下,才能使用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命令来获得坐标(所有这些数字),该命令将返回可以剪切和粘贴的坐标。

第一个帧如下:

Figure 8.5 - Frame 0 and scene DS0

图 8.5 -第 0 帧和场景 DS0

  1. 我们现在改变场景,为进入蛋白质内部做准备:

    cmd.frame(200)
    cmd.scene('S1')
    cmd.mview()
    

下面的截图显示了当前位置:

Figure 8.6 - Frame 200 near the DNA molecule and scene S1

图 8.6 -第 200 帧附近的 DNA 分子和场景 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()
    

我们现在完全在里面了,如下面的截图所示:

Figure 8.7 - Frame 350 – scene S1 on the verge of changing to 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):

Figure 8.8 - Frame 450 and scene 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 扩展)。反过来使用它(在没有 GUI 的情况下从 Python 导入)可能会很复杂和令人沮丧。PyMOL 启动一个单独的线程来呈现异步工作的图像。

例如,这意味着您的代码可能位于与呈现器不同的位置。我已经将另一个名为PyMol_Intro.py的脚本放在了 GitHub 库中;您将看到第二个 PNG 调用将在第一个调用完成之前开始。尝试脚本代码,看看您期望它如何运行,以及它实际上是如何运行的。

www.pymolwiki.org/index.php/MovieSchool有大量从 GUI 角度看 PyMOL 的好文档。如果你想拍电影,这是一个很好的起点,而www.pymolwiki.org是一个信息宝库。

使用 Biopython 解析 mmCIF 文件

mmCIF 文件格式很可能是未来。Biopython 还没有完整的功能来使用它,但我们会看看目前存在的功能。

准备就绪

由于Bio.PDB不能自动下载 mmCIF 文件,您需要获取您的蛋白质文件并将其重命名为1tup.cif。这个可以在1TUP.cif下的github . com/packt publishing/Bioinformatics-with-Python-Cookbook-third-Edition/blob/master/datasets . py找到。

你可以在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解析器有更多的发展。

PDB 的开发者在 http://mmcif . wwpdb . org/docs/SW-examples/Python/html/index . XHTML 提供了一个 Python 库。

九、生物信息管道

管道是任何数据科学环境中的基础。数据处理从来都不是一项单一的任务。许多管道是通过专用脚本实现的。这可以用一种有用的方式来实现,但是在许多情况下,它们不符合几个基本的观点,主要是可再现性、可维护性和可扩展性。

在生物信息学中,你可以找到三种主要类型的管道系统:

在这一章中,我们将讨论 Galaxy,它对于支持不太倾向于自己编写解决方案的用户的生物信息学家来说尤其重要。虽然您可能不是这些管道系统的典型用户,但您可能仍然需要支持它们。幸运的是,Galaxy 提供了 API,这将是我们的主要关注点。

我们还将讨论 Snakemake 和 Nextflow,它们是起源于生物信息学领域的具有编程接口的通用工作流工具。我们将涵盖这两种工具,因为它们是该领域中最常见的。我们将使用 Snakemake 和 Nextflow 解决一个类似的生物信息学问题。我们将尝试两种框架,并希望能够决定最喜欢的一种。

这些食谱的代码并不是以笔记本的形式出现,而是以 Python 脚本的形式出现在该书知识库的Chapter09目录中。

在本章中,您将找到以下配方:

  • 银河服务器简介
  • 使用 API 访问 Galaxy
  • 用 Snakemake 开发变体分析管道
  • 用 netflow 开发变体分析管道

银河服务器简介

银河(galaxyproject.org/tutorials/g101/)是一个开源系统,让非计算用户能够做计算生物学。这是最广泛使用的,用户友好的管道系统。任何用户都可以将 Galaxy 安装在一台服务器上,但是网上也有很多其他的服务器可以公开访问,旗舰服务器是 http://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
    

这就拉了 bjrn grüning 的惊人的 Docker 星系图像。使用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。如果出现任何错误,请等待几秒钟。您应该会看到以下屏幕:

Figure 9.1 - The Galaxy Docker home page

图 9.1 -银河 Docker 主页

  1. 现在用默认的用户名和密码组合:adminpassword登录(见顶栏)。
  2. 从顶部菜单中选择用户,在里面选择偏好
  3. 现在,选择管理 API 密钥

不要更改 API 密钥。前面练习的目的是让您知道 API 键在哪里。在实际情况下,您必须到此屏幕获取您的密钥。只需注意 API 键:fakekey。顺便说一下,在正常情况下,这将是一个 MD5 散列。

因此,在这个阶段,我们用以下(默认)凭证安装了服务器:用户名为admin,密码为password,API 密匙为fakekey。接入点是localhost:8080

还有更多

bjrn grüning 的形象在本章中的使用方式非常简单;毕竟这不是一本关于系统管理或者 DevOps 的书,而是一本编程的书。如果你访问 https://github.com/bgruening/docker-galaxy-stable,你会发现有无数种方法来配置镜像,并且都有很好的文档记录。我们这里的简单方法适用于我们的开发目的。

如果你不想在你的本地电脑上安装 Galaxy ,你可以使用一个公共服务器如usegalaxy.org来做下一个菜谱。这不是 100%万无一失的,因为服务会随着时间的推移而变化,但可能会非常接近。采取以下步骤:

  1. 在公共服务器上创建一个帐户(usegalaxy.org或其他)。
  2. 按照前面的说明访问您的 API 密钥。
  3. 在下一个方法中,您必须替换主机、用户、密码和 API 密钥。

使用 API 访问 Galaxy

虽然 Galaxy 的主要用例是通过一个易于使用的 web 界面,但它也提供了一个用于编程访问的 REST API。有几个语言提供的接口,比如 bio blend(bio blend . readthedocs . io提供 Python 支持。

在这里,我们将开发一个脚本,将一个 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,并指定我们将创建的星系历史的名称(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. 最后,我们能够连接到银河服务器:

    gi = GalaxyInstance(url=rest_url, key=api_key)
    gi.verify = False
    
  3. 我们现在将列出所有可用的histories:

    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)
    

如果你愿意,你可以在网络界面上查看,你会在那里找到新的历史记录。

  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. 让我们检查一下文件

    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 界面,您将看到类似于以下内容的内容:

Figure 9.2 - Checking the results of our script via Galaxy’s web interface

图 9.2 -通过 Galaxy 的 web 界面检查我们脚本的结果

因此,我们已经使用 REST API 访问了银河。

使用 Snakemake 部署变体分析管道

Galaxy 主要面向不太喜欢编程的用户。知道如何处理它是很重要的,因为它无处不在,即使你更喜欢对程序员友好的环境。令人欣慰的是,有一个 API 可以与 Galaxy 交互。但是如果你想要一个对程序员更友好的管道,有很多选择。在这一章中,我们探索两个广泛使用的程序员友好的管道:snakemake和 Nextflow。在这个食谱中,我们考虑snakemake

Snakemake 是用 Python 实现的,与它有许多共同的特点。也就是说,它的基本灵感是 Makefile,古老的make构建系统使用的框架。

这里,我们将使用snakemake开发一个迷你变体分析管道。这里的目标不是让科学部分正确——我们将在其他章节讨论——而是看看如何用 ?? 创建管道。我们的迷你管道将下载 HapMap 数据,以 1%的比例对其进行二次采样,进行简单的 PCA,并绘制它。

准备就绪

您需要将 Plink 2 安装在snakemake旁边。为了显示执行策略,您还需要 Graphviz 来绘制执行。我们将定义以下任务:

  1. 下载数据
  2. 解压缩它
  3. 以 1%的比例进行子采样
  4. 计算 1%子样本的主成分分析
  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 脚本。该字符串是一个 Pythonf-string-注意该字符串中对snakemake inputoutput变量的引用。您可以在这里放置更复杂的 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构造了一个操作的有向无环图 ( DAG )来执行。在任何时候,您都可以要求snakemake向您展示一个 DAG,您将执行它来生成您的请求。例如,要生成 PCA,请使用以下代码:

    snakemake --dag data/hapmap1.eigenvec | dot -Tsvg > bio.svg
    

这将生成下面的图:

Figure 9.3 - The DAG to compute the PCA

图 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. 最后,代码产生一个粗略图表如下:

Figure 9.4 - A very rough PCA produced by the Snakemake pipeline

图 9.4-snake make 管道产生的非常粗糙的 PCA

还有更多

前面的配方在一个简单的配置snakemake上运行。在snakemake中有更多的方法来构造规则。

我们没有讨论的最重要的问题是,snakemake可以在许多不同的环境中执行代码,从本地计算机(如我们的例子)、本地集群到云。要求使用本地计算机来尝试snakemake是不合理的,但是不要忘记snakemake可以管理复杂的计算环境。

请记住,snakemake虽然是用 Python 实现的,但在概念上是基于make的。这是一个主观分析,以决定你是否喜欢(蛇)作出的设计。对于另一种设计方法,检查下一个配方,它使用 Nextflow。

使用 Nextflow 部署变体分析管道

生物信息学中的流水线框架空间有两个主要玩家:snakemake和 Nextflow。它们提供流水线功能,同时具有不同的设计方法。Snakemake 基于 Python,但它的语言和哲学来自于用于编译具有依赖关系的复杂程序的make工具。Nextflow 是基于 Java 的(更准确地说,它是用 Groovy 实现的——一种在 Java 虚拟机上工作的语言),并且有自己的领域特定语言 ( DSL )用于实现管道。这个配方(和之前的配方)的主要目的是给你一个 Nextflow 的味道,这样你就可以和snakemake比较,选择一个更适合你需求的。

小费

关于如何评估管道系统,有许多观点。在这里,我们基于用于指定管道的语言呈现一个透视图。然而,在选择管道系统时,你还应该考虑其他因素。例如,它在多大程度上支持您的执行环境(比如本地集群或云),它是否支持您的工具(或者允许轻松开发扩展来处理新工具),它是否提供良好的恢复和监控功能?

在这里,我们将使用 Nextflow 开发一个管道,提供与我们使用snakemake实现的相同的功能,从而允许从管道设计的角度进行公平的比较。这里的目标不是让科学部分正确——我们在其他章节中讨论——而是看看如何用snakemake创建管道。我们的迷你管道将下载 HapMap 数据,以 1%对其进行子采样,进行简单的 PCA,并绘制它。

准备就绪

您需要将 Plink 2 与 Nextflow 一起安装。Nextflow 本身需要一些来自 Java 领域的软件:特别是 Java 运行时环境和 Groovy。

我们将定义以下任务:

  1. 下载数据
  2. 解压缩它
  3. 以 1%的比例进行子采样
  4. 计算 1%子样本的主成分分析
  5. 绘制 PCA 图

可以在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. 我们可以通过在 shell 上运行以下命令来执行工作流:

    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:

    process plink_pca {
      input:
      path 'hapmap.map'
      path 'hapmap.ped'
      output:
      path 'hapmap.eigenvec'
      path 'hapmap.eigenval'
       script:
      """
      plink2 --pca --pedmap hapmap -out hapmap
      """
    }
    

    计算 PCA

  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中找到。

在本章中,我们将介绍以下配方:

  • 介绍 sci kit-通过 PCA 示例学习
  • 使用 PCA 上的聚类对样本进行分类
  • 利用决策树探索乳腺癌特征
  • 使用随机森林预测乳腺癌结果

介绍 sci kit-通过 PCA 示例学习

PCA 是一种统计程序,用于将多个变量的维度降低到一个更小的线性不相关的子集。在 第六章 中,我们看到了一个基于使用外部应用的 PCA 实现。在这个菜谱中,我们将为群体遗传学实现相同的 PCA,但将使用scikit-learn库。Scikit-learn 是用于机器学习的基本 Python 库之一,本菜谱是对该库的介绍。PCA 是一种无监督的机器学习形式——我们不提供关于样本类别的信息。我们将在本章的其他配方中讨论监督技术。

提醒一下,我们将从 HapMap 项目中计算 11 个人类群体的 PCA。

准备就绪

您将需要运行第六章 中 的第一个配方,以便生成hapmap10_auto_noofs_ld_12 PLINK 文件(等位基因记录为 1 和 2)。从群体遗传学的角度来看,我们需要 LD-pruned 标记来产生可靠的 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-D 坐标。

    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)
    

Figure 10.1 - PC1 to PC8 for our dataset as produced by scikit-learn

图 10.1-scikit-learn 生成的数据集的 PC1 到 PC8

还有更多...

对于在科学期刊上发表的,我会推荐使用第六章中的配方,仅仅因为它是基于一个已发表的且备受推崇的方法。也就是说,这段代码的结果在性质上是相似的,并且以非常相似的方式对数据进行聚类(如果与第六章图进行比较,垂直轴上方向的反转与解释 PCA 图无关)。

利用 PCA 上的聚类对样本进行分类

基因组学中的主成分分析让我们看到样本是如何聚集的。在许多情况下,来自同一人群的个体会出现在图表的同一区域。但是我们想更进一步,预测新个体在群体中的位置。为了做到这一点,我们将从 PCA 数据开始,因为它可以降维——使数据处理更容易——然后应用 K-Means 聚类算法来预测新样本的位置。我们将使用与上面食谱中相同的数据集。我们将使用除一个样本之外的所有样本来训练算法,然后我们将预测剩余样本的位置。

k 均值聚类可以是监督算法的一个例子。在这些类型的算法中,我们需要一个训练数据集,以便算法能够学习。在训练算法之后,它将能够为新的样本预测某个结果。在我们的例子中,我们希望能够预测人口数量。

警告

当前的食谱旨在温和地介绍监督算法及其背后的概念。我们训练算法的方式远非最佳。正确训练监督算法的问题将在本章的最后一个配方中提到。

准备就绪

我们将使用与之前配方中相同的数据。该配方的代码可在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. 我们现在将所有样本数据——SNP——加载到一个 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 均值聚类的训练集的 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)
    

下面是输出,这将有助于检查聚类结果:

Figure 10.2 - PC1 and PC2 with populations color-coded

图 10.2 - PC1 和 PC2 带有颜色编码的人口

  1. 在我们开始计算 K-means 聚类之前,让我们编写一个函数,通过运行算法

    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)
    

这里是输出:

Figure 10.3 - The cluster surface for 11 clusters

图 10.3-11 个集群的集群表面

如果您与这里的图进行比较,您可以直观地看到聚类没有什么意义:它没有很好地映射到已知的人口。有人可能会认为这种具有 11 个聚类的聚类算法不是很有用。

小费

scikit-learn 中实现了许多其他聚类算法,在一些场景中,它们可能比 K-means 执行得更好。你可以在 https://scikit-learn.org/stable/modules/clustering.xhtml 找到它们。值得怀疑的是,在这个特定的情况下,对于 11 个集群来说,任何替代方案都不会有更好的表现。

  1. 虽然看起来 K-means 聚类不能解析 11 个群体,但是如果我们使用不同数量的聚类,它仍然可以提供一些预测。简单地看一下图表,我们可以看到四个独立的区块。如果我们使用四个集群会有什么结果?

    kmeans4 = KMeans(n_clusters=4).fit(trans)
    plot_kmeans_pca(trans, kmeans4)
    

以下是输出:

Figure 10.4 - The cluster surface for four clusters

图 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 的值进行编码。有关该项目的更多信息,请访问 http://archive . ics . UCI . edu/ml/datasets/breast+cancer+Wisconsin+% 28 diagnostic % 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'])
    

此产生以下输出:

Figure 10.5 - The decision tree for the breast cancer dataset

图 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 的介绍;在下面的食谱中,我们将进行并行处理。我们的项目将计算单个染色体上所有基因组位置的缺失率。

准备就绪

疟蚊 1000 个基因组数据可从谷歌云平台 ( GCP )获得。要从 GCP 下载数据,你需要gsutil,可从cloud.google.com/storage/docs/gsutil_install获得。安装完gsutil后,下载数据(~ 2GB(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 文件有五个数组:四个对应于蚊子的染色体——2L2R3L3RX ( Y不包括在内)——其中一个有一个包含 81 个样本的列表。最后一个数组包含了样本名称—这个文件中有 81 个样本。染色体数据由 8 位整数(int8)组成,样本名称为字符串。

  1. 现在,让我们探索染色体2L的数据。先说一些基本信息:

    gt_2l = mosquito['/2L/calldata/GT']
    gt_2l
    

以下是输出:

<zarr.core.Array '/2L/calldata/GT' (48525747, 81, 2) int8>

对于81样本,我们有一系列4852547 单核苷酸多态性 ( SNPs )。对于每个 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 兆字节 ( MB )。这意味着压缩比为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 还有的替代格式——比如分层数据格式 5(HD F5)(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. 我们将使用与上一个配方完全相同的函数来计算统计数据——这主要是基于数字的:

    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 中线程管理的实现方式,线程化并不是真正并行处理的可行替代方案。纯 Python 代码不能使用多线程并行运行。

您可能使用的一些库 NumPy 通常就是这种情况——能够利用所有底层处理器,即使是在执行一段连续的代码时。确保在使用外部库时,不要过度使用处理器资源:当您有多个进程时会出现这种情况,并且底层库也会使用许多内核。

参见

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

Dask 是一个提供高级并行的库,可以从单台计算机扩展到非常大的集群或云操作。它还提供了处理大于内存的数据集的能力。它能够提供类似于常见 Python 库(如 NumPy、Pandas 或 scikit-learn)的接口。

我们将重复之前食谱中的例子的子集,即计算数据集中 SNPs 的缺失。我们将使用与 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. 让我们首先加载数据并检查数据帧的大小:

    import numpy as np
    import dask.array as da
    
    mosquito = da.from_zarr('data/rechunk')
    mosquito
    

如果您在 Jupyter 中执行,下面是输出:

Figure 11.1 - Jupyter output for a Dask array, summarizing our data

图 11.1-Dask 数组的 Jupyter 输出,总结了我们的数据

整个数组占用了7.32 GB。最重要的数字是块大小:1.83 GB。每个工作者将需要有足够的内存来处理一个块。请记住,我们仅使用如此少量的块来绘制这里的任务。

由于块的大小很大,我们最终只有四个块。

我们还没有在内存中加载任何东西:我们只是指定了我们最终想要做的事情。我们正在创建一个要执行的任务图,而不是执行——至少现在是这样。

  1. 让我们看看加载数据需要执行哪些任务:

    mosquito.visualize()
    

以下是输出:

Figure 11.2 - Tasks that need to be executed to load our Zarr array

图 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()
    

    进行整形

以下是目前的任务图表:

Figure 11.3 - The task graph to load genomic data and reshape it

图 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 非常聪明,只向计算中涉及的块添加了一个操作:

Figure 11.4 - The complete task graph including statistical computing

图 11.4 -包括统计计算的完整任务图

  1. 请记住,到目前为止,我们还没有计算任何东西。现在是实际执行任务图的时候了:

    stats = stats.compute() 
    

这将开始计算。精确的计算是如何完成的,我们将在下一个食谱中讨论。

警告

由于块的大小,这段代码可能会使你的计算机崩溃。至少有 16 GB 的内存,你会很安全。记住,你可以使用更小的块大小——你应该使用更小的块大小。我们只是使用这样的块大小,以便能够生成前面显示的任务图(如果不是这样,它们可能有数百个节点,并且不可打印)。

还有更多...

我们在这里没有花任何时间讨论优化 Dask 代码的策略——那将是一本独立的书。对于非常复杂的算法,您需要进一步研究最佳实践。

Dask 提供了类似于其他已知 Python 库(如 Pandas 或 scikit-learn)的接口,可用于并行处理。您也可以将它用于不基于现有库的一般算法。

参见

使用 dask.distributed 调度任务

Dask 在执行方面非常灵活:我们可以在本地执行,在科学的集群上执行,或者在云上执行。这种灵活性是有代价的:它需要参数化。配置 Dask 调度和执行有几种选择,但最通用的是dask.distributed,因为它能够管理不同种类的基础设施。因为我不能假设你可以访问集群或者云,比如亚马逊网络服务 ( AWS )或者 GCP,我们将在你的本地机器上设置计算,但是记住你可以在非常不同的平台上设置dask.distributed

在这里,我们将再次对按蚊 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 数据框架,Dask 可能会选择一个多处理器调度器。由于 Pandas 不是并行的,Dask 本身并行运行是有意义的。

好了——现在我们已经了解了重要的细节,让我们回到准备我们的环境。

有一个集中的调度器和一组工人,我们需要启动它们。在命令行中运行以下代码启动调度程序:

dask-scheduler --port 8786 --dashboard-address 8787

我们可以在与调度程序相同的机器上启动 workers,如下所示:

dask-worker --nprocs 2 –nthreads 1 127.0.0.1:8786

我指定了两个进程,每个进程有一个线程。这对于 NumPy 代码来说是合理的,但是实际的配置将取决于您的工作负载(如果您在集群或云上,这将完全不同)。

小费

您实际上不需要像我在这里做的那样手动启动整个过程。如果您自己没有准备好系统,将会为您启动一些东西,但并不真正针对您的工作负载进行优化(有关详细信息,请参见下一节)。但是我想让您感受一下这种努力,因为在许多情况下,您必须自己建立基础设施。

同样,我们将使用来自第一个配方的数据。确保你下载了并做好准备,正如它的准备部分所解释的。我们将不使用重新分块的部分——我们将在下一节的 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 上,你会得到一个很好的输出,总结了你在这个菜谱的准备好部分创建的配置:

Figure 11.5 - Summary of your execution environment with dask.distributed

图 11.5-dask . distributed 的执行环境概要

您会注意到这里对仪表板的引用。dask.distributed为提供了一个网络上的实时仪表盘,允许你跟踪计算的状态。将您的浏览器指向 http://127.0.0.1:8787/以找到它,或者只需跟随图 11.5 中提供的链接。

因为我们还没有进行任何计算,所以仪表板基本上是空的。请务必浏览顶部的许多选项卡:

Figure 11.6 - The starting state of the dask.distributed dashboard

图 11.6-dask . distributed 仪表板的启动状态

  1. 让我们加载数据。更严谨一点,让我们准备加载数据的任务图:

    mosquito = da.from_zarr('data/AG1000G-AO/2L/calldata/GT')
    mosquito
    

下面是 Jupyter 上的输出:

Figure 11.7 - Summary of the original Zarr array for chromosome 2L

图 11.7 -染色体 2L 的原始 Zarr 阵列总结

  1. 为了便于形象化,让我们再来一次。我们还将为第二个维度准备一个单独的块,也就是样本。这是因为我们对缺失的计算需要所有的样本,在我们的具体例子中,每个样本有两个块是没有意义的:

    mosquito = mosquito.rechunk((mosquito.shape[0]//8, 81, 2))
    

提醒一下,我们有非常大的块,你可能会有内存问题。如果是这种情况,那么您可以使用原始块运行它。只是可视化会不可读。

  1. 在我们继续之前,让我们要求 Dask 不仅要执行重新分块,还要在工人中准备好结果:

    mosquito = mosquito.persist()
    

persist调用确保数据在工人中可用。在下面的截图中,您可以在计算过程中的某个地方找到仪表板。您可以找到每个节点上正在执行的任务、已完成和待完成任务的摘要,以及每个工作者存储的字节数。值得注意的是溢出到磁盘的概念(见屏幕左上角)。如果没有足够的内存存储所有区块,它们将被临时写入磁盘:

Figure 11.8 - The dashboard state while executing the persist function for rechunking the array

图 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 数组上工作。这是当前的任务图表。没有加载数据的操作,因为前面执行的函数执行了所有这些操作:

Figure 11.9 - Calls to the calc_stats function over chunks starting with persisted data

图 11.9 -从持久数据开始对块调用 calc_stats 函数

  1. 最后,我们可以得到我们的结果:

    stat_results = stats.compute()
    

还有更多...

关于dask.distributed接口还有很多可以说的。在这里,我们介绍了其架构和仪表板的基本概念。

dask.distributed提供基于 Python 的标准async模块的异步接口。由于这一章的介绍性质,我们不会涉及它,但建议你看一看。

参见

十二、生物信息学的函数式编程

Python 是一种多范式语言,允许你以多种不同的方式表达计算。它有时被称为面向对象的 ( OO )语言:当然,你可以用 OO 方言写代码,但是你也可以使用其他风格。Python 中的大多数代码都是以命令式风格编写的:没有沿着类层次结构的结构,这是面向对象范式的典型特征,大多数代码都改变了状态,例如,如果你写了x = x + 1,你就改变了变量x的状态。

如果你写复杂的代码,特别是需要并行处理的代码,命令式和面向对象的范例会遇到一些限制。对于在单台机器上运行的简单脚本,命令式和面向对象式就可以了,但是生物信息学是一个大数据企业,您通常需要纵向扩展和横向扩展,因为有大量信息要处理,并且许多算法计算量很大。

函数式编程对于生物信息学中越来越常见的复杂和并行问题非常有用。许多用于高吞吐量数据分析的现代架构都是基于函数式编程思想的,例如,MapReduce 范式最初是在 Google 和 Apache Spark 等平台上开发的。这些思想也直接适用于 Dask,甚至使顺序代码更加健壮。

函数式编程是基于函数,纯函数求值,避免可变性。在这一章中,我们将采用一种非常实用的方法来介绍这些概念,并举例说明它们在典型的生物信息学应用中的用例。最后,您应该对函数式编程有一个基本的概念理解,但最重要的是,您将理解它的实用性和适用性。

如果您正在使用 Docker,并且因为所有这些库都是数据分析的基础,所以它们都可以在 Docker 映像tiagoantao/bioinformatics_base中找到。

在本章中,我们将介绍以下配方:

  • 理解纯函数
  • 理解不变性
  • 避免可变性作为健壮的开发模式
  • 使用惰性编程实现流水线操作
  • Python 递归的局限性
  • Python 的functools模块展示

理解纯函数

一个纯函数有几个重要的属性:对于相同的输入,它产生相同的输出,并且它没有副作用(它不改变全局变量,也不做 I/O)。在这个食谱中,我们将介绍几个简单的例子来阐明这个概念。我们最感兴趣的是第二个特性:没有副作用。在后面的食谱中,将会清楚为什么纯函数会非常有用。

我们将开发一个非常简单的例子,在这个例子中,我们对每个样本中测序的基因进行计数。我们将有一个文本文件的基因计数数据库。例如,我们可能在一个样本上测定了 LCT 和 TP53 的序列,在另一个样本上测定了 LCT、MRAP2 和 POMC 的序列。总数将是:TP53: 1,LCT: 2,MRPA2: 1,POMC: 1。我们将使用一个 CSV 文件,可以很容易地阅读熊猫,甚至只是 CSV 模块。

准备就绪

我们将使用一个简单的 CSV 文件作为我们的小数据库。该配方的代码可在Chapter12/Pure.py中找到。数据库文件是my_genes.csv,在my_genes.csv.base有一个保存的原始状态,以防你需要回到原始数据库状态。

怎么做...

看看下面的步骤就可以开始了:

  1. 让我们首先创建两个简单的函数来加载和保存数据。我们还需要一些时间来恢复原始数据库:

    import shutil
    import pandas as pd
    
    def restore_db(file_name):
        shutil.copyfile(f'{file_name}.base', file_name)
    
    def load(file_name):
        df = pd.read_csv(file_name).set_index('gene')
        return dict(df['count'])
    
     def save(dict_db, file_name):
        pd.Series(dict_db).to_csv(
            file_name, index_label='gene', header=['count'])
    

我们用熊猫来照顾坚持。这是一个非常简单的例子;您也可以只使用csv模块。

  1. 我们将考虑三个替代函数来报告在样本中看到的基因;下面是第一个:

    def add_sample_csv(gene_list):
        gene_count = load('my_genes.csv')
        for gene in gene_list:
            gene_count[gene]=gene_count(gene,0)+1
        save(gene_count, 'my_genes.csv')
    

该函数自动用新样本保存数据。它有读写文件的副作用,所以它不是一个纯粹的函数。

  1. 这里有一个第二选择,也报告样本中的基因:

    def add_sample_global_dict(gene_list):
        global gene_count
        for gene in gene_list:
            gene_count[gene] = gene_count.get(0) + 1
    

这个函数使用模块中的一个全局变量并更新它,这是一个副作用。我们将不使用这个函数,但它是作为一个有副作用的函数的例子提供的。我们应该避免全局变量。

  1. 下面是一个纯函数变体:

    def add_sample_dict(dict_db, gene_list):
        for gene in gene_list:
            dict_db[gene] = dict_db.get(0) + 1
        return dict_db
    

这个函数改变了它的一个参数——dict_db——正如我们将在下一个菜谱中看到的,从函数式编程的角度来看,这不是一个最佳实践。然而,对于相同的输出,它总是返回相同的结果,并且没有副作用,所以这将是一个很好的第一种方法。

  1. 假设我们现在运行以下代码:

    add_sample_csv(['MC4R', 'TYR'])
    

但是,如果我们错误地运行了 10 次,会有什么后果呢?从逻辑角度来看,我们会将两个基因都多报 9 次,从而破坏我们数据库的内容。

  1. 作为一种选择,考虑以下:

    add_sample_dict(gene_count, ['MC4R', 'TYR'])
    

如果运行 10 次,会有什么后果?这仍然是一个错误(因为我们正在改变gene_count),但至少它还没有被提交到磁盘。在进行探索性的数据分析时,这种方言会舒服得多——您可以在知道不会损坏外部数据的情况下运行它。在下一个食谱中,我们将看到一种替代方法,使重新运行代码问题更少。

还有更多...

在接下来的几个食谱中,我们将通过非常实际的案例来了解为什么纯函数会非常有用。但是有一个具体的例子很难解释,因此,我们将在这里介绍这个理论。

纯函数使得并行化代码更加容易。想象一个执行分布式计算的框架,比如 Dask 或 Spark。这种框架必须处理硬件故障的潜在情况,代码需要被移动并可能在其他地方重复。如果您的代码在每次运行时都写入磁盘(这是一个副作用的例子),那么分布式框架的恢复就要复杂得多。如果您的代码没有副作用,那么分布式框架可以重复您的功能,而无需考虑数据一致性。事实上,许多分布式框架不允许在可并行化代码中产生副作用。他们也可能反对数据结构的可变性,这是我们现在要讨论的。

理解不变性

函数式编程的另一个共同特点是数据结构通常是不可变的。当你习惯于命令式编程时,这是一个很难理解的概念——命令式编程是指在没有对象的情况下编程,对象的状态会随着时间的推移而改变。在这里,我们将看到一个简单的例子,用一种不可变的方式使前面的配方中的函数工作:也就是说,没有对象被改变,如果我们需要传递新的信息,我们就创建新的。

这个菜谱将从数据结构的角度对不变性做一个简短的介绍。从某种意义上来说,这将是你能在大多数书中找到的标准演示。然而,我们主要考虑的是将可变性作为一种代码设计模式来讨论,这也是下面食谱的主题。但对于这一点,我们需要先理解不变性。

我们将研究两个函数:一个会改变数据结构,另一个不会。这将在我们在本章前面的食谱中遵循的例子的上下文中完成。

准备就绪

我们将使用与之前配方相同的数据。该配方的代码可在Chapter12/Mutability.py中找到。

怎么做...

请遵循以下步骤:

  1. 根据前面的配方,我们有一个改变输入字典的函数:

    def add_sample_dict(dict_db, gene_list):
        for gene in gene_list:
            dict_db.get(gene,0) +1
    

如果你用add_sample_dict(gene_count, ['DEPP'])调用这个代码,而没有输出参数,你的字典的gene_count将会是突变的,以将1添加到基因DEPP中。如果您运行这个函数的次数过多——这在进行探索性数据分析时很常见——您可能会错误地更改字典。

  1. 将前面的实现与下面的进行对比:

    def add_sample_new_dict(dict_db, gene_list):
        my_dict_db = dict(dict_db)
        for gene in gene_list:
            my_dict_db[gene] = my_dict_db.get(0) + 1
        return my_dict_db
    

在这种情况下,我们将dict_db复制到一个新的字典my_dict_db,并添加额外的基因列表。制作现有词典的副本需要耗费内存和时间,但是原始词典dict_db没有改变。

  1. 如果您使用这个实现,您肯定您的输入参数永远不会改变:

    new_gene_count = add_sample_new_dict(gene_count, ['DEPP'])
    

gene_count不是变的,new_gene_count是一本全新的词典。您可以重复执行任意多次,而不必担心每次执行的影响。

还有更多...

这个简单的方法提供了一个不改变参数的函数的例子。我们现在的情况是,我们可以想执行多少次这个函数就执行多少次——无论是错误的还是故意的——而不会对代码的其余部分产生任何影响。这在我们测试代码或运行探索性数据分析时非常有用,因为我们不必担心副作用。这也方便了分布式执行器(如 Dask 或 Spark)的工作,因为它们不必担心检查现有对象的状态:它们是固定的。

对于一般的软件设计来说,这里也有重要的教训,即使你不使用分布式执行器。这就是我们在下面的食谱中要研究的内容。

避免可变性是一种健壮的开发模式

前面的配方引入了不可变数据结构的概念。在这份食谱中,我们将讨论一种设计模式,它可以避免代码中的持久数据库可变性,直到最后。就伪代码而言,长脚本中的大多数应用程序如下工作:

Do computation 1
Write computation 1 to disk or database
Do computation 2
Write computation 2 to disk or database
….
Do computation n
Write computation n to disk or database

在这里,我们将展示一个替代范例,并讨论为什么从弹性的角度来看它通常更好:

Do computation 1
Write computation 1 to temporary storage
Do computation 2
Write computation 2 to temporary storage
...
Do computation n
Write computation n to temporary storage
Take all temporary data and write it to definitive disk and database

首先,我们将展示这两种方法的代码,然后讨论为什么对于复杂的脚本,后者在大多数情况下是更好的方法。

我们将使用与前面食谱中相同的例子:我们将报告在不同样品中看到的基因。

准备就绪

我们将使用与之前配方中相同的数据。该配方的代码可在Chapter12/Persistence1.pyChapter12/Persistence2.py中找到。

怎么做...

让我们用一些共享代码来设置这两个解决方案。

  1. 两种解决方案仍然使用loadsave函数:

    import pandas as pd
    
    def restore_db(file_name):
        shutil.copyfile(f'{file_name}.base', file_name)
    
    def load(file_name):
        df = pd.read_csv(file_name).set_index('gene')
        return dict(df['count'])
    
    def save(dict_db, file_name):
        pd.Series(dict_db).to_csv(
            file_name, index_label='gene', header=['count'])
    
  2. 第一种选择是,我们边走边存,如下所示:

    def add_sample_csv(gene_list):
        gene_count = load('my_genes.csv')
        for gene in gene_list:
            gene_count[gene]=gene_count(gene,0)+1
        save(gene_count, 'my_genes.csv')
    
    add_sample_csv(['MC4R', 'TYR'])
    add_sample_csv(['LCT', 'HLA-A'])
    add_sample_csv(['HLA-B', 'HLA-C'])
    

每当我们添加一个样本,我们就更新主数据库。虽然从 I/O 的角度来看,这种解决方案不是非常有效,但这不是这里要讨论的问题——实际上,从 I/O 的角度来看,大多数这种设计模式往往比我们接下来要看的更有效。

  1. 当我们在最后存储最终数据时,第二个选择如下:

    def add_sample_new_dict(dict_db, gene_list):
        my_dict_db = dict(dict_db)  # next recipe
        for gene in gene_list:
            dict_db.get(gene,0) +1
        return my_dict_db
    
    gene_count = load('my_genes.csv')
    gene_count = add_sample_new_dict(gene_count, ['MC4R', 'TYR'])
    gene_count = add_sample_new_dict(gene_count, ['LCT', 'HLA-A'])
    gene_count = add_sample_new_dict(gene_count, ['HLA-B', 'HLA-C'])
    save(gene_count, 'my_genes.csv')
    

在这种情况下,我们只在最后更新主数据库。我们使用内存字典来维护中间数据。

还有更多...

那么,为什么要这样做呢?延迟写入最终数据的解决方案更加复杂,因为我们必须潜在地存储部分结果并对其进行管理(在这种情况下,我们使用gene_count字典——这是一个简单的例子)。事实上,这是真的,但代码有 bug,磁盘存储空间填满,计算机在现实世界中会崩溃,所以从务实的角度来看,面对这一点是更重要的考虑因素。

假设您的第一个解决方案由于某种原因在执行过程中停止了工作。数据库处于未知状态,因为您不知道代码在哪里。您不能从零开始重新启动代码,因为它的一部分可能已经被执行了。因此,您必须检查数据库的状态,查看代码在哪里停止,并部分运行丢失的部分,或者找到一种方法回滚执行的部分。它既麻烦又容易出错。

现在,假设在第二种方法的执行过程中出现了一个问题:您不必担心数据的中间状态,因为它不会影响您的最终数据库,并且会在第二次执行中完全重新计算。您可以重新运行代码,而不用担心不一致的状态。唯一的例外是最后一行,但这不仅不太常见,而且您还可以将所有精力集中在这一点上,以使流程尽可能具有弹性。

这能一直做到吗?还是只在某些情况下有效?大多数代码都可以创建临时文件。此外,许多生物信息学分析不是实时事务性的,所以通常只在最后才更新 SQL 数据库很容易。现在,您有了一个与整个代码相关的单点故障——如果有问题,您知道您只需要关注最后的部分。

在可能的情况下,这种方法将使复杂代码的日常维护更加容易。

使用惰性编程进行流水线操作

懒惰编程是一种策略,我们把计算推迟到真正需要的时候。与急切编程相比,它有许多优点,在急切编程中,我们一调用计算就计算所有的东西。

Python 为懒惰编程提供了许多机制——事实上,从 Python 2 到 Python 3 的最大变化之一就是语言变得更加懒惰。

为了理解懒惰编程,我们将再次使用我们的基因数据库,并用它做一个练习。我们将检查是否至少有 n 个基因,每个基因有 y 个读数(例如,三个基因,每个基因有五个读数)。比方说,这可以是对我们数据库质量的一种衡量——也就是说,衡量我们是否有足够的基因和一定数量的样本。

我们将考虑两种实现:一种是懒惰的,另一种是渴望的。然后我们将比较这两种方法的含义。

准备就绪

该配方的代码可在Chapter12/Lazy.py中找到。

怎么做...

看看这两种不同的方法:

  1. 让我们从急切的实现开始:

    import pandas as pd
    def load(file_name):
        df = pd.read_csv(file_name).set_index('gene')
        return dict(df['count'])
    
    def get_min_reads(all_data, min_reads):
        return {
            gene: count
            for gene, count in all_data.items()
            if count >= min_reads
        }
    
    def has_min_observations(subset_data, min_observations):
        return len(subset_data) >= min_observations
    print(has_min_observations(
        get_min_reads(
            load('my_genes.csv'), 4
        ), 3))
    

注意代码加载所有数据,检查所有条目的计数4,并查看是否至少有3个具有该计数的观察值。

  1. 这是一个另类,懒惰的版本:

    def get_rec(file_name):
        with open(file_name) as f:
            f.readline()  # header
            for line in f:
                toks = line.strip().split(',')
                yield toks[0], int(toks[1])
    
    def gene_min_reads(source, min_reads):
        for gene, count in source:
            if count >= min_reads:
                yield gene
    
    def gene_min_observations(subset_source, min_observations):
        my_observations = 0
        for gene in subset_source:
            my_observations += 1
            if my_observations == min_observations:
                return True
        return False
    print(gene_min_observations(
        gene_min_reads(
            get_rec('my_genes.csv'), 4
        ), 2))
    

这段代码以一种完全不同的方式运行。首先,get_recgene_min_reads是生成器(注意yield,所以它们只会在需要的时候返回结果,一个接一个。gene_min_observations一旦观察到所需的观察次数,就会返回。这意味着将被读取和处理的唯一数据是达到结果的最少数据。在最坏的情况下,这仍然是整个文件,但在许多情况下,它可以少得多。

还有更多...

对于非常大的文件,这种方法的优势显而易见。虽然我们的玩具文件没有明显的优势,但是如果文件太大而不适合内存,第一种方法就会崩溃!此外,管道的每一步都需要足够的内存来存储它消耗的所有数据,并且所有的步骤都需要同时在内存中。因此,即使对于中等大小的文件,内存问题也可能在急切版本中出现,而惰性版本可以避免这种问题。

很多时候,惰性代码做的处理也少得多:就像前面的例子一样,它可能会在看到所有数据之前就停止计算。这并不总是确定的,但在许多用例中都会发生。

从历史的角度来看,Python 2 比 Python 3 更有热情。这实际上是从 Python 2 到 Python 3 的主要变化之一。举个简单的例子,考虑一下 Python 2 和 3 中range的行为。

我们的惰性实现的一部分可以通过使用一些 Python 内置模块以更简洁的方式编写——我们将在最终的食谱中再次讨论这一点。

使用 Python 递归的局限性

递归——一个调用自己的函数——是函数式编程中的一个基本概念。许多迎合函数式编程的语言能够无限递归。Python 不是这些语言中的一种。你可以在 Python 中使用递归,但是你必须意识到它的局限性,即递归会导致相当大的内存开销,并且它不能用来代替迭代——这是编写函数式程序时的典型情况。

为了了解 Python 递归的限制,我们将以几种方式实现斐波那契数列。提醒一下,斐波那契可以定义如下:

Fib(0) = 0
Fib(1) = 1
Fib(n) = Fib(n -1) + Fib(n –2)

我们也在计算阶乘函数,但在这种情况下只是以递归的方式:

Fact(1) = 1
Fact(n) = n * Fact(n –1 )

准备就绪

我们的代码可以在Chapter12/Recursion.py中找到。

怎么做...

按照以下步骤开始:

  1. 先说一个迭代版的斐波那契:

    def fibo_iter(n):
        if n < 2:
            return n
        last = 1
        second_last = 0
        for _i in range(2, n + 1):
            result = second_last + last
            second_last = last
            last = result
        return result
    

这个版本已经足够了,而且也非常高效。我们并不是说迭代版本在任何方面都不如递归版本。相反,用 Python,可能会表现得更好。

  1. 下面是一个递归版本:

    def fibo_naive(n):
        if n == 0:
            return 0
        if n == 1:
            return 1
        return fibo_naive(n - 1) + fibo_naive(n - 2)
    

如果您运行它,您会发现与迭代版本相比,性能受到了相当大的影响。此外,如果您要求fibo_naive(1000)或一个类似的大数字,代码将根本不能很好地执行(1000 个案例可能需要很多小时),而迭代版本将充分执行。在接下来的食谱中,我们将实际修复其中的一部分。但是现在,让我们更深入地研究递归和 Python 的问题。

  1. 为了让的情况变得非常明显,让我们用实现一个阶乘函数,从递归实现的角度来看,这个函数尽可能简单(甚至比斐波那契更简单):

    def factorial(n):
        if n == 1:
            return 1
        return n * factorial(n - 1)
    

如果你用一个小数字运行这个函数,比如factorial(5),你会得到正确的答案,5。但是如果你尝试一个大的数字,比如factorial(20000),你会得到如下结果:

Traceback (most recent call last):
  File "Recursion.py", line 8, in <module>
    factorial(20000)
  File "Recursion.py", line 4, in factorial
    return n * factorial(n - 1)
  File "Recursion.py", line 4, in factorial
    return n * factorial(n - 1)
  File "Recursion.py", line 4, in factorial
    return n * factorial(n - 1)
  [Previous line repeated 995 more times]
  File "Recursion.py", line 2, in factorial
    if n == 1:
RecursionError: maximum recursion depth exceeded in comparison

这个错误提示在 Python 中你可以递归多少是有限制的:Python 只能递归到某个限制(见sys.setrecursionlimit来改变这一点)。虽然您可以将递归的数量变得更大,但是维护堆栈所需的内存总会有一个限制。还有其他方法可以显式实现递归,但代价是速度。许多语言都实现了一个称为尾部调用优化 ( TCO )的特性,它允许高性能的无限级递归,但是 Python 没有实现它。

还有更多...

在 Python 中,您可以对不需要多次循环调用的简单情况使用递归,但是在 Python 中,递归并不是迭代解决方案的一般替代品。递归可能是 Python 函数世界中实现得最差的主要特性。

Python 的 functools 模块展示

Python 有三个内置模块,在用函数式方言编写代码时非常有用:functoolsoperatoritertools。在本食谱中,我们将简要讨论functools模块。例如,基本的reduce功能(这是 MapReduce 名字的一部分)只有在导入functools时才可用。

虽然对这些模块的详细探究对于单个配方来说太长了,但是我们将通过使用functools中的功能改进之前配方的一些代码来展示一些功能,并展示模块效用的一些说明性示例。

准备就绪

我们的代码可以在Chapter12/Builtin.py中找到。我们将参考以前的食谱。

怎么做...

让我们看几个说明性的例子:

  1. 还记得我们在前面的配方中阶乘函数的递归实现不是很高效吗?让我们用一种非常简单的方式来缓解它的许多问题:

    import functools
    
    @functools.cache
    def fibo(n):
        if n == 0:
            return 0
        if n == 1:
            return 1
        return fibo(n - 1) + fibo(n - 2)
    

如果你记得前一个配方中的递归代码,它非常慢,即使对于小数字也是如此。做这件事可能需要几个小时。这个函数只需添加@functools.cache装饰器,就可以更快地处理更多的数字。这是由于cache装饰器实现了记忆化。

什么是记忆化?

记忆化是这样一个过程,计算机通过它缓存执行的结果,并在下一次调用它时,不再次计算它,而只是返回存储的结果。例如,第一次调用fibo(10),为了得到结果,实际上执行了函数,但是第二次调用时,结果在第一次运行时被缓存,没有执行就返回了。只有当函数总是为相等的输入返回相同的输出,并且没有副作用时,记忆化才能正确工作。也就是说,记忆化只对纯函数起作用。

  1. 对于另一个使用函数方法替代现有代码的示例,从使用惰性编程进行流水线操作方法:

    def gene_min_reads(source, min_reads):
        for gene, count in source:
            if count >= min_reads:
                yield gene
    

    中获取函数

这里已经有了一些功能性的味道,因为我们使用了一个生成器。

  1. 这个函数可以用一种功能性更强的方言来编写:

    def gene_min_reads(source, min_reads):
        return map(
            lambda x: x[0],
            filter(lambda x: x[1] >= min_reads,
            source.items()))
    

这里有很多东西需要打开。首先看内置的filter函数:它会将第一个参数中定义的函数应用到第二个参数上迭代器的对象上。在我们的例子中,第二个元素大于或等于min_reads的对象将被保留。然后,map函数获取每个对象(类型为('GENE', count))并只返回第一部分。mapfilter函数在函数式编程的方言中非常常见。还要注意匿名函数的典型函数概念,a lambda:这是一个只在一个地方使用的函数定义——它对非常小的函数非常有用。在这种情况下,没有直接的理由以这种方式重写(特别是因为前面定义的生成器类型已经为我们的问题提供了函数式方言的最重要的特性),但是您会发现在许多情况下这种类型的表示更加简洁和有表现力。

  1. 另一个使用分布式系统时你可能需要的重要概念是部分函数应用——这里有一个使用最基本算术函数的简单例子:

    def multiplication(x, y):
        return x * y
    double = functools.partial(multiplication, 2)
    double(3)
    

double被定义为部分解决multiplication中的一个参数——因此,我们使用术语部分函数应用。这不仅仅是一个风格上的问题,因为有时在分布式平台中(或者甚至仅仅是多处理池的map函数),您被限制为只能提供一个参数——因此,部分函数应用程序成为一种必要。

还有更多...

functools模块中有许多更有趣的应用程序可以查看。从功能的角度来看,itertoolsoperator模块也有许多惯用的功能。

参见...

Python 有几个函数式编程库,例如,参见以下内容:

posted @ 2024-08-10 15:28  绝不原创的飞龙  阅读(2)  评论(0编辑  收藏  举报