文章

WGDI的简单使用及预处理扩展

简单介绍WGDI的使用方法,以及预处理过程。

WGDI的简单使用及预处理扩展

WGDI (Whole-Genome Duplication Integrated analysis) is a Python-based command-line tool designed to simplify the analysis of whole-genome duplications (WGD) and cross-species genome alignments.

WGDI(全基因组重复整合分析)是一款基于 Python 的命令行工具,旨在简化全基因组重复(WGD)和跨物种基因组比对分析。

https://github.com/SunPengChuan/wgdi

物种的进化总是有延续性的——这一点在基因组与染色体组上,就体现为多物种之间的同源区段及其动态变化。它们有时会产生多拷贝,有时会随着时间而打散甚至消失,还有一部分又重新整合,形成一个全新的基因簇。这些动态往往反映了生物的进化历史,是确定生物分化时间、亲缘关系等的直接证据之一。

WGDI就是在全基因组范围内,分析物种内与物种间重复(多拷贝)与整合(重组成基因簇)的工具。它的功能比较丰富,例如绘制 dot-plot、获取共线性参数、计算 Ks 值,等等。不同功能需要不同的配置文件,但是前置要求是一样的。你需要的文件包括:

  • blast结果(采用format 6。可以用 NCBI-BLAST+,也可以用针对大规模数据优化的diamond);
  • GFF与LEN文件(需要特定脚本实现,有格式要求)。

其他参数将在后面介绍。

安装方法

你可以用pypipip)或者conda安装它:

1
2
pip3 install wgdi              # pypi
conda install bioconda::wgdi   # conda

如果使用conda但还没用过bioconda,请先在终端里执行这些指令:

1
2
3
conda config --add channels bioconda
conda config --add channels conda-forge
conda config --set channel_priority strict

如果经历这种错误:

1
2
envs/wgdi/lib/python3.11/site-packages/Bio/Application/__init__.py:39: BiopythonDeprecationWarning: The Bio.Application modules and modules relying on it have been deprecated.                       
......

卸载 WGDI 后重新执行安装:

conda create -n wgdi python=3.12 biopython wgdi

问题即可解决。

预处理过程

这里的 GFF 和 LEN 文件都不是标准格式文件。WGDI 对这两个文件的格式要求如下:

  • 对于 GFF:

    列数列名解释
    1染色体该基因所在染色体代号
    2ID该基因的标识符
    3起点基因头部在该染色体上的碱基位置
    4终点基因尾部在该染色体上的碱基位置
    5方向基因所在链的方向(+/-)
    6顺序该基因在该染色体上的顺序。从 1 开始
    7原始信息原始ID或附加信息,可无。不用于分析
  • 对于LEN:

    列数列名解释
    1染色体该染色体代号
    2长度该染色体的碱基长度
    3基因数该染色体上的基因数量

怎么重构?我们来看GFF原始文件。它包含主条目与各子条目,例如,这是取自 Ensembl Rapid 上的其中的“两个”条目:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
###
1	ensembl	ncRNA_gene	749	1456	.	+	.	ID=gene:ENSFBZG00000004380;biotype=lncRNA;gene_id=ENSFBZG00000004380;version=1
1	ensembl	lnc_RNA	749	1456	.	+	.	ID=transcript:ENSFBZT00000006931;Parent=gene:ENSFBZG00000004380;biotype=lncRNA;tag=Ensembl_canonical;transcript_id=ENSFBZT00000006931;version=1
1	ensembl	exon	749	868	.	+	.	Parent=transcript:ENSFBZT00000006931;constitutive=0;exon_id=ENSFBZE00000031713;rank=1;version=1
1	ensembl	exon	947	1456	.	+	.	Parent=transcript:ENSFBZT00000006931;constitutive=0;exon_id=ENSFBZE00000031716;rank=2;version=1
###
1	ensembl	gene	6948	8703	.	+	.	ID=gene:ENSFBZG00000000663;biotype=protein_coding;gene_id=ENSFBZG00000000663;version=1
1	ensembl	mRNA	6948	8703	.	+	.	ID=transcript:ENSFBZT00000001038;Parent=gene:ENSFBZG00000000663;biotype=protein_coding;tag=Ensembl_canonical;transcript_id=ENSFBZT00000001038;version=1
1	ensembl	five_prime_UTR	6948	7244	.	+	.	Parent=transcript:ENSFBZT00000001038
1	ensembl	exon	6948	7566	.	+	.	Parent=transcript:ENSFBZT00000001038;constitutive=0;exon_id=ENSFBZE00000005188;rank=1;version=1
1	ensembl	CDS	7245	7566	.	+	0	ID=CDS:ENSFBZP00000001038;Parent=transcript:ENSFBZT00000001038;protein_id=ENSFBZP00000001038;version=1
1	ensembl	CDS	7841	8439	.	+	2	ID=CDS:ENSFBZP00000001038;Parent=transcript:ENSFBZT00000001038;protein_id=ENSFBZP00000001038;version=1
1	ensembl	exon	7841	8703	.	+	.	Parent=transcript:ENSFBZT00000001038;constitutive=0;exon_id=ENSFBZE00000005190;rank=2;version=1
1	ensembl	three_prime_UTR	8440	8703	.	+	.	Parent=transcript:ENSFBZT00000001038
###

是的,这并非是“12个”条目,而是两个—— ensembl 把不同的主条目用 ### 行隔开。在这里,每个条目的第一行就是我们要找的“主条目”了。我们只需要gene就好,另一个是ncRNA_gene,它不编码蛋白质,所以在这里并没有用。这可以靠检索 GFF 的第三列来确定。

再来看每一列,它们代表的应该是

1
染色体	来源	类型	起点	终点	得分	方向	CDS相位	其他属性

而 WGDI 需要的格式是

1
染色体	基因代号	起点	终点	方向	在该染色体上的顺序

第1、3、4、5可通过 GFF 直接获得(原第1、4、5、7),基因代号可通过原 GFF 的其他属性(第8列)获得,而最后一列通过染色体列间接获得。

先解决前五列:

1
2
3
4
5
6
7
8
9
10
11
12
    # GFF3 解析
    gff_data = pd.read_csv(args.ingff3, sep="\t", header=None, comment="#", dtype={0: str, 3: int, 4: int, 8: str})
    gff_data.columns = ['chr', 'source', 'feature', 'start', 'end', 'score', 'strand', 'phase', 'attribute']

    # 重新提取数据:'feature' = type 参数的 dataframe
    gff_data = gff_data[gff_data['feature'] == args.type].loc[:, ['chr', 'attribute', 'start', 'end', 'strand']]

    # 取各 feature 的实际 args.type
    gff_data['attribute'] = gff_data['attribute'].str.extract(r'ID=([^;]+)')

    # 重命名
    gff_data.columns = ['chr', args.type, 'start', 'end', 'strand']

最后一列可以通过重做索引的方法实现:

1
2
3
    # 重做索引
    gff_data = gff_data.sort_values(by=['chr', 'start']).reset_index(drop=True)
    gff_data['order'] = gff_data.groupby('chr').cumcount() + 1

我们还可以借此机会生成LEN文件,它需要所有染色体长度与基因数信息。染色体长度可通过该染色体最后一个基因的终点间接确定(虽然不太准确,但在没有DNA测序的情况下可作为近似);基因数通过第一列里有该染色体的行数来确定。

1
2
3
4
5
6
7
8
9
10
11
12
    # 统计各染色体核苷酸长度
    chr, lens = [], []
    for rec in SeqIO.parse(args.infna, 'fasta'):
        chr.append(rec.id)
        lens.append(len(rec.seq))
    chr2length = pd.DataFrame(data={'chr': chr, 'lens': lens})

    # 聚合方法,统计 'attribute' 条目数(count)
    chr2count = gff_data.groupby('chr').agg({args.type: 'count'}).reset_index()

    # 合并
    lens = pd.merge(chr2length, chr2count, on='chr')

综上,生成GFF和LEN的任务可以合并到一个脚本来完成(1_get_gff_len.py)。


我们需要的3个文件,现在已经搞定了2个。还差个blastp的结果——可是蛋白质组数据的问题,还没有解决!这样如何获得blast结果嘛!

有人会说,获取蛋白质数据,完全可以直接用 gffread 来实现嘛!这确实可行,但实际操作的时候,还是会差点事。

例如,每一个蛋白质条目变成了转录本,基因与蛋白质的对应关系消失了(特别常见于 ensembl 版的 GFF):

1
> transcript:ENSWHXT00000003709

另一种,基因已知,但分成了若干转录本(一般课题组自己获得的就是这种):

1
2
> g11.t1
> g11.t2

我们显然不希望发生这两种情况。好在这俩其实从根本上是同一个问题——如果一个基因条目关联到多个转录本,如何选择?我们更加倾向于选择更长的转录本。例如这种:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
###
1	ensembl	gene	123687	127517	.	+	.	ID=gene:ENSWHXG00000001281...
1	ensembl	mRNA	123687	127124	.	+	.	ID=transcript:ENSWHXT00000002004...
1	ensembl	five_prime_UTR	123687	123766	.	+	.	Parent=transcript:ENSWHXT00000002004...
1	ensembl	exon	123687	124332	.	+	.	Parent=transcript:ENSWHXT00000002004...
1	ensembl	CDS	123767	124332	.	+	0	ID=CDS:ENSWHXP00000002004...
1	ensembl	exon	124433	124766	.	+	.	Parent=transcript:ENSWHXT00000002004...
1	ensembl	CDS	124433	124766	.	+	1	ID=CDS:ENSWHXP00000002004...
1	ensembl	exon	124974	125247	.	+	.	Parent=transcript:ENSWHXT00000002004...
1	ensembl	CDS	124974	125247	.	+	0	ID=CDS:ENSWHXP00000002004...
1	ensembl	CDS	125767	126125	.	+	2	ID=CDS:ENSWHXP00000002004...
1	ensembl	exon	125767	127124	.	+	.	Parent=transcript:ENSWHXT00000002004...
1	ensembl	three_prime_UTR	126126	127124	.	+	.	Parent=transcript:ENSWHXT00000002004...
1	ensembl	mRNA	123687	127517	.	+	.	ID=transcript:ENSWHXT00000002017...
1	ensembl	five_prime_UTR	123687	123766	.	+	.	Parent=transcript:ENSWHXT00000002017...
1	ensembl	exon	123687	124332	.	+	.	Parent=transcript:ENSWHXT00000002017...
1	ensembl	CDS	123767	124332	.	+	0	ID=CDS:ENSWHXP00000002017...
1	ensembl	exon	124433	124766	.	+	.	Parent=transcript:ENSWHXT00000002017...
1	ensembl	CDS	124433	124766	.	+	1	ID=CDS:ENSWHXP00000002017...
1	ensembl	exon	124974	125247	.	+	.	Parent=transcript:ENSWHXT00000002017...
1	ensembl	CDS	124974	125247	.	+	0	ID=CDS:ENSWHXP00000002017...
1	ensembl	exon	125767	125948	.	+	.	Parent=transcript:ENSWHXT00000002017...
1	ensembl	CDS	125767	125948	.	+	2	ID=CDS:ENSWHXP00000002017...
1	ensembl	CDS	126507	126518	.	+	0	ID=CDS:ENSWHXP00000002017...
1	ensembl	exon	126507	127517	.	+	.	Parent=transcript:ENSWHXT00000002017...
1	ensembl	three_prime_UTR	126519	127517	.	+	.	Parent=transcript:ENSWHXT00000002017...
###

好复杂!但稍微精简一下,结构就一目了然:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
###
1	gene	123687	127517	.	+	gene:ENSWHXG00000001281
1	mRNA	123687	127124	.	+	transcript:ENSWHXT00000002004
1	CDS	123767	124332	.	+
1	CDS	124433	124766	.	+
1	CDS	124974	125247	.	+
1	CDS	125767	126125	.	+
1	mRNA	123687	127517	.	+	transcript:ENSWHXT00000002017
1	CDS	123767	124332	.	+
1	CDS	124433	124766	.	+
1	CDS	124974	125247	.	+
1	CDS	125767	125948	.	+
1	CDS	126507	126518	.	+
###

很明显,一个gene带了两个transcript!怎么做呢?将它们做差再求和,然后取CDS最长的那个版本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
transcript:ENSWHXT00000002004
124332-123767+1 = 566
124766-124433+1 = 334
125247-124974+1 = 274
126125-125767+1 = 359
sum = 1533

transcript:ENSWHXT00000002017
124332-123767+1 = 566
124766-124433+1 = 334
125247-124974+1 = 274
125948-125767+1 = 182
126518-126507+1 = 12
sum = 1368

此时ENSWHXG00000001281对应的“唯一”转录本为ENSWHXT00000002004。我们首先要记住这个对应关系,然后将这个转录本“抽”出来,最后还要把转录本的名字换成基因的名字。——这可能吗?

过程显然要比上一个脚本复杂一些,但不一定无法完成。整理一下思路,我们首先要找到每个基因当中 CDS 最长的那个转录本,然后用这个转录本从基因组中翻译出蛋白质序列,最后用基因的名字命名序列。

同样的,我们还是要请 Pandas 来帮我们组织一下数据。我们的流程是——

  • 利用 mRNA 做第一次”遍历”, 建立基因 -> 转录本的关系(虽然看上去是“遍历”,但其实是每一行同时进行处理)。

    1
    2
    3
    4
    5
    6
    7
    
        # 建立基因到转录本(mRNA)的关系
        print(f"\033[94mAnalysing\033[0m make dictionary for gene to transcript...")
        g2t_df = pd.DataFrame(columns=['gene', 'transcript', 'chromosome'])
        g2t_df['gene'] = mrna_data['attribute'].str.extract(r"Parent=([^;]+)")
        g2t_df['transcript'] = mrna_data['attribute'].str.extract(r"ID=([^;]+)")
        g2t_df['chromosome'] = mrna_data['chr']
      
    
  • 利用 CDS 做第二次”遍历”, 获得转录本 CDS 长度。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
        # 计算 CDS 区长度,以便选出 CDS 最长的转录本
        print(f"\033[94mAnalysing\033[0m calculating transcript CDS length...")
        cds_data = cds_data.loc[:, ['attribute', 'start', 'end']].copy()
        cds_data['transcript'] = cds_data['attribute'].str.extract(r"Parent=([^;]+)")
        cds_data['start'] = cds_data['start'].astype(int)
        cds_data['end'] = cds_data['end'].astype(int)
        cds_data['length'] = cds_data['end'] - cds_data['start'] + 1
        cds_length_df = cds_data.groupby('transcript')['length'].sum().reset_index()
      
    
  • 通过公共项“转录本名称”合并1和2的结果,然后,通过选择 CDS 最长的转录本,形成转录本 -> 基因一一对应的关系(字典)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
        # 合并两个dataframe,然后找最长的转录本
        print(f"\033[94mAnalysing\033[0m search the longest CDS...")
        transcript_table = pd.merge(g2t_df, cds_length_df, on='transcript')
        transcript_table = transcript_table.loc[:, ['gene', 'transcript', 'length', 'chromosome']]
        transcript_table_filtered = transcript_table.loc[transcript_table.groupby('gene')['length'].idxmax()]
        transcript_table_filtered['gene_order'] = transcript_table_filtered.groupby('chromosome').cumcount() + 1
      
        # 收集结果(转录本到基因、染色体编号等的关系)
        print(f"\033[94mAnalysing\033[0m collate result...")
        print(transcript_table_filtered)
        trans2gene = dict(zip(transcript_table_filtered['transcript'], transcript_table_filtered['gene']))
        trans2chr = dict(zip(transcript_table_filtered['transcript'], transcript_table_filtered['chromosome']))
        trans2order = dict(zip(transcript_table_filtered['transcript'], transcript_table_filtered['gene_order']))
      
    
  • 最后,在gffread的结果里:

    1. 删掉字典里没有出现的转录本;
    2. 将转录本名字换成基因名字。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    # 对生成的序列进行筛选
    # 若序列label与transcript库匹配,则取用
    filtered_records_cds = [
        record for record in SeqIO.parse(args.outfna, "fasta") if record.id in list(transcript_table_filtered['transcript'])
    ]
    filtered_records_prot = [
        record for record in SeqIO.parse(args.outfaa, "fasta") if record.id in list(transcript_table_filtered['transcript'])
    ]

    # 将transcript名称改为基因名称
    for idx, record in enumerate(filtered_records_cds):
        record.description = f"index={idx+1},gene={trans2chr[record.id]}g{trans2order[record.id]}"
        record.id = trans2gene[record.id]
    SeqIO.write(filtered_records_cds, args.outfna, "fasta")

    for idx, record in enumerate(filtered_records_prot):
        record.description = f"index={idx+1},gene={trans2chr[record.id]}g{trans2order[record.id]}"
        record.id = trans2gene[record.id]
        record.seq = record.seq.replace(old='.', new="X")
    SeqIO.write(filtered_records_prot, args.outfaa, "fasta")
    

具体流程可以参考第二个文件 2_get_prot.py

至此,大部分命令所需的全部文件已经集齐。

正式的数据处理过程

WGDI功能非常丰富,它可以通过不同类型的配置文件,实现不同的功能:

-d:dot-plot,做同源基因点图,可作为文献插图使用。 -icl:collinearity,获得共线性结果,本质是改进版的 ColinearScan。进一步分析时可以利用。 -ks:计算同源基因对的 Ka/Ks。 -bi:blockinfo,将共线性与Ks结果合并到一起。

这里我们只演示 -d-icl 的用法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# dotplot.conf
[dotplot]
blast = blast file
blast_reverse = false
gff1 =  gff1 file
gff2 =  gff2 file
lens1 = lens1 file
lens2 = lens2 file
genome1_name =  Genome1 name
genome2_name =  Genome2 name
multiple = 1
score = 100
evalue = 1e-5
repeat_number = 10 
position = order 
blast_reverse = false 
ancestor_left = none 
ancestor_top = none
markersize = 0.5
figsize = 10,10
savefig = savefile(.svg,.png,.pdf)

之后再 wgdi -d dotplot.conf

等待出图。

然后,执行wgdi -icl coll.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# coll.conf
[collinearity]
gff1 = gff1 file
gff2 = gff2 file
lens1 = lens1 file
lens2 = lens2 file
blast = blast file
blast_reverse = false
multiple = 1 # 用红点显示的同源基因的最佳数量
process = 8 # 使用进程数量
evalue = 1e-5
score = 100 # 和点图保持一致
grading = 50,40,25 # 红,蓝,灰的不同分值,按照分值会优先连红色的点,蓝色点次之
mg = 40,40 # 两个基因对被认为能连起来的最大距离(罚分)
pvalue = 0.2 # 评估共线性的指标,如果想全部提取可以选1。共线块的compactness and uniqueness,范围为0-1,较好的共线范围为0-0.2
repeat_number = 10 # 和点图保持一致 
positon = order # 唯一选项
savefile = collinearity file # 输出文件

结果大概是这个样子(MCScanX风格):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# Alignment 1: score=401 pvalue=0.1706 N=15 ped_chr001&Chr01 plus
g1362 1242 Pfi01G025700 2570 1
g1363 1243 Pfi01G025830 2583 -1
g1385 1262 Pfi01G025960 2596 1
g1401 1276 Pfi01G026040 2604 -1
g1434 1306 Pfi01G026060 2606 1
g1435 1307 Pfi01G026080 2608 1
g1451 1322 Pfi01G026310 2631 1
g1452 1323 Pfi01G026320 2632 1
g1457 1328 Pfi01G026350 2635 1
g1460 1330 Pfi01G026380 2638 1
g1465 1334 Pfi01G026400 2640 1
g1466 1335 Pfi01G026440 2644 1
g1470 1339 Pfi01G026460 2646 1
g1485 1354 Pfi01G026700 2670 1
g1492 1360 Pfi01G026710 2671 1
# Alignment 2: score=383 pvalue=0.0002 N=8 ped_chr001&Chr01 plus
g634 586 Pfi01G036910 3691 1
g635 587 Pfi01G036920 3692 1
g636 588 Pfi01G036940 3694 1
g637 589 Pfi01G036960 3696 1
g638 590 Pfi01G036970 3697 1
g639 591 Pfi01G036980 3698 -1
g640 592 Pfi01G036990 3699 -1
g641 593 Pfi01G037010 3701 -1
......

一点点的Q&A

如果 Chr 顺序不太对,或者名称太长互相叠叠乐?

想修改染色体及其顺序,可以直接动 LEN 文件。

改名可以通过另一个脚本来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
"""
map文件的参考格式:
旧染色体名1\t新染色体名1
旧染色体名2\t新染色体名2
旧染色体名3\t新染色体名3
......

获得方法:执行 cat 'FNA文件路径' | grep '>' > map.txt,然后补上 Tab,删掉多余内容即可。
"""

import argparse
import re

from Bio import SeqIO

def switch_dict(map_path: str) -> dict:
    dictionary = {}
    with open(map_path, "r") as map:
        for line in map:
            line_ls = line.strip().split("\t")
            dictionary[line_ls[0]] = line_ls[1]
    return dictionary

def default_output_name(filename):
    name, ext = filename.rsplit('.', 1)
    return f"{name}_changed.{ext}"

def main():
    print()
    parser = argparse.ArgumentParser(description="Renaming the chromosome name in fna/gff3 file.")

    parser.add_argument('--infna', required=True, help='Input FNA file name.')
    parser.add_argument('--ingff', required=True, help='Input GFF3 file name.')
    parser.add_argument('--inmap', required=True, help='Old and new Chr name, in TSV format.')
    parser.add_argument('--outfna', required=False, help='Output FNA file name. Optional.')
    parser.add_argument('--outgff', required=False, help='Output GFF3 file name. Optional.')

    args = parser.parse_args()
    outfna = args.outfna if args.outfna else default_output_name(args.infna)
    outgff = args.outgff if args.outgff else default_output_name(args.ingff)

    d = switch_dict(args.inmap)
    fna_record = []
    gff_record = ""
    
    # 更改 FNA 里的条目
    for idx, rec in enumerate(SeqIO.parse(args.infna, "fasta")):
        # 改名;若无对应则保持原状
        try: rec.id = d[rec.id]
        except KeyError: rec.id = rec.id
        # 添加记录
        fna_record.append(rec)

    # 写入
    SeqIO.write(fna_record, outfna, "fasta")

    # 更改 GFF 里的条目
    pattern = re.compile("|".join(re.escape(k) for k in d.keys()))
    with open(args.ingff) as gff_original, open(outgff, "w") as gff_changed:
        for idx, rec in enumerate(gff_original):
            gff_new = pattern.sub(lambda m: d[m.group(0)], rec)
            gff_record = gff_record + gff_new
        gff_changed.write(gff_record)
    
    print("DONE!")

if __name__ == "__main__":
    main()

右侧和下方有很多 Chr 叠在一起,很不好看?

可以直接在 LEN 文件里,删掉基因数量非常少的,没有组装到染色体上的 Chr,然后再重新作图。

接下来该如何做?例如美化共线性结果?或者针对某个基因座进行更精细的共线性分析?

这部分推荐交给 jcvi,或者 TBtools-II,然后使用 -icl 的结果。不过——如果想作图,建议先改 LEN 文件,把散在的 Chr 删掉,再跑 WGDI,效果会更好。

改 e-value 或者得分阈值,对结果影响大吗?

e-value的影响不是很大。得分阈100可行,200差异不大,但500会破坏共线性关系。因此直接用默认参数就好。

能否只使用一个配置文件?

可以。

运行 wgdi -xxxx ? > total.conf-xxxx = -d/-icl/…),即可生成单功能的配置模板,然后就可以根据自己的需求,补充缺失的参数。继续生成时,把 > 改成 >> 即可。

但这就有了一个问题:同一个文件下,不同 section(一个 [] 即一个 section)下可能使用同一个参数,例如 gff len 之类。能否只动一处,就能改变全局的同名参数?

关键在于 .ini 文件写法。可以先设一个 [universal] section,把公共参数写进去,其他 section 调用时,则使用 ${universal:argument}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[universal]
blast = BLAST_RES/Pedel2Pvulg.f6
gff1 =  GFF/Pedel_Simp.gff
gff2 =  GFF/Pvulg_Simp.gff
lens1 = LEN/Pedel.len
lens2 = LEN/Pvulg.len
......

[dotplot]
blast = ${universal:blast}
gff1 =  ${universal:gff1}
gff2 =  ${universal:gff2}
lens1 = ${universal:lens1}
lens2 = ${universal:lens2}

但代价是要在源码上做改动。在 WGDI 源码中,需要对 base.py 的第 29 行进行修改:

1
2
3
# envs/py312/lib/python3.12/site-packages/wgdi/base.py
- conf = configparser.ConfigParser()
+ conf = configparser.ConfigParser(interpolation=configparser.ExtendedInterpolation())

这能让 ConfigParser 同时兼容这种简并写法。

附录

处理脚本源码

附上这两个脚本的源代码,各位可以随意取用,或者进一步改进。这些代码是我在做本科毕设的共线性分析时写的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
"""
WGDI Pre-Process Step-1

以下预处理代码改进自原 WGDI 项目。
使用前请确认是否安装了 Pandas 和 BioPython。

GitHub主页: https://github.com/SunPengChuan/wgdi?tab=readme-ov-file
相关论文: https://doi.org/10.1016/j.molp.2022.10.018
脚本原始来源:https://lxz9.com/2021/09/06/wgdi/#%E8%84%9A%E6%9C%AC
"""

import argparse
import pandas as pd
from Bio import SeqIO

def main():
    print()
    print("WGDI Pre-Process \033[94mStep-1\033[0m")

    # 用法说明与参数解析
    parser = argparse.ArgumentParser(description="WGDI Pre-Process Step-1: Get GFF and LEN Files.")
    parser.add_argument('--infna', required=True, help='Input FNA file name.')
    parser.add_argument('--ingff3', required=True, help='Input GFF3 file name.')
    parser.add_argument('--outgff', required=True, help='Output simplified GFF file name.')
    parser.add_argument('--outlen', required=True, help='Output simplified LEN file name.')
    parser.add_argument('--type', required=True, help='Entries type.')
    args = parser.parse_args()

    print(f"\033[94mProcessing\033[0m [{args.ingff3}] >>> [{args.outgff}, {args.outlen}]")

    # --- GFF ---
    # GFF3 解析
    gff_data = pd.read_csv(args.ingff3, sep="\t", header=None, comment="#", dtype={0: str, 3: int, 4: int, 8: str})
    gff_data.columns = ['chr', 'source', 'feature', 'start', 'end', 'score', 'strand', 'phase', 'attribute']

    # 重新提取数据:'feature' = type 参数的 dataframe
    gff_data = gff_data[gff_data['feature'] == args.type].loc[:, ['chr', 'attribute', 'start', 'end', 'strand']]

    # 取各 feature 的实际 args.type
    gff_data['attribute'] = gff_data['attribute'].str.extract(r'ID=([^;]+)')

    # 重命名
    gff_data.columns = ['chr', args.type, 'start', 'end', 'strand']

    # 重做索引
    gff_data = gff_data.sort_values(by=['chr', 'start']).reset_index(drop=True)
    gff_data['order'] = gff_data.groupby('chr').cumcount() + 1

    # 输出 GFF
    print(f"\033[94mWriting\033[0m to {args.outgff}...")
    print(gff_data)
    gff_data.to_csv(args.outgff, sep="\t", header=False, index=False)

    # --- LEN ---
    print(f"\033[94mCalculating\033[0m chromosome lengths and gene counts...")
    # 基于 FNA 文件,统计各染色体核苷酸长度
    chr, lens = [], []
    for rec in SeqIO.parse(args.infna, 'fasta'):
        chr.append(rec.id)
        lens.append(len(rec.seq))
    chr2length = pd.DataFrame(data={'chr': chr, 'lens': lens})

    # 对原 gff_data 采用聚合方法,统计 'attribute' 条目数(count)
    chr2count = gff_data.groupby('chr').agg({args.type: 'count'}).reset_index()

    # 合并两个 dataframe
    lens = pd.merge(chr2length, chr2count, on='chr')

    # 输出 LENS
    print(f"\033[94mWriting\033[0m to {args.outlen}...")
    print(lens)
    lens.to_csv(args.outlen, sep="\t", header=False, index=False)

    print("\033[92mPre-Process Step-1 Complete!\033[0m")
    print()

if __name__ == "__main__":
    main()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
"""
WGDI Pre-process Step-2

以下预处理代码改进自原 WGDI 项目。
使用前请确认是否安装了 gffread,
是否在 Python 环境中安装了 Pandas 与 BioPython。

GitHub主页: https://github.com/SunPengChuan/wgdi?tab=readme-ov-file
相关论文: https://doi.org/10.1016/j.molp.2022.10.018
脚本原始来源:https://lxz9.com/2021/09/06/wgdi/#%E8%84%9A%E6%9C%AC
"""

from Bio import SeqIO

import pandas as pd
import subprocess as sp
import argparse

def main():
    print()
    print("WGDI Pre-Process \033[94mStep-2\033[0m")

    # 用法说明与参数解析
    parser = argparse.ArgumentParser(description="WGDI Pre-Process Step-2: Get Protein FASTA Files.")
    parser.add_argument('--ingff3', required=True, help='Input GFF3 file name.')
    parser.add_argument('--infna', required=True, help='Input FASTA(Genome) file name.')
    parser.add_argument('--outfna', required=True, help='Output FASTA(CDS) file name.')
    parser.add_argument('--outfaa', required=True, help='Output FASTA(Protein) file name.')
    args = parser.parse_args()

    # GFF解析
    gff_data = pd.read_csv(args.ingff3, sep="\t", header=None, comment="#", dtype=str)
    gff_data.columns = ['chr', 'source', 'feature', 'start', 'end', 'score', 'strand', 'phase', 'attribute']

    # 提取包含mRNA和CDS的数据
    mrna_data = gff_data[gff_data['feature'] == "mRNA"]
    cds_data = gff_data[gff_data['feature'] == "CDS"]

    # 建立基因到转录本(mRNA)的关系
    print(f"\033[94mAnalysing\033[0m make dictionary for gene to transcript...")
    g2t_df = pd.DataFrame(columns=['gene', 'transcript', 'chromosome'])
    g2t_df['gene'] = mrna_data['attribute'].str.extract(r"Parent=([^;]+)")
    g2t_df['transcript'] = mrna_data['attribute'].str.extract(r"ID=([^;]+)")
    g2t_df['chromosome'] = mrna_data['chr']

    # 计算 CDS 区长度,以便选出 CDS 最长的转录本
    print(f"\033[94mAnalysing\033[0m calculating transcript CDS length...")
    cds_data = cds_data.loc[:, ['attribute', 'start', 'end']].copy()
    cds_data['transcript'] = cds_data['attribute'].str.extract(r"Parent=([^;]+)")
    cds_data['start'] = cds_data['start'].astype(int)
    cds_data['end'] = cds_data['end'].astype(int)
    cds_data['length'] = cds_data['end'] - cds_data['start'] + 1
    cds_length_df = cds_data.groupby('transcript')['length'].sum().reset_index()

    # 合并两个dataframe,然后找最长的转录本
    print(f"\033[94mAnalysing\033[0m search the longest CDS...")
    transcript_table = pd.merge(g2t_df, cds_length_df, on='transcript')
    transcript_table = transcript_table.loc[:, ['gene', 'transcript', 'length', 'chromosome']]
    transcript_table_filtered = transcript_table.loc[transcript_table.groupby('gene')['length'].idxmax()]
    transcript_table_filtered['gene_order'] = transcript_table_filtered.groupby('chromosome').cumcount() + 1

    # 收集结果(转录本到基因、染色体编号等的关系)
    print(f"\033[94mAnalysing\033[0m collate result...")
    print(transcript_table_filtered)
    trans2gene = dict(zip(transcript_table_filtered['transcript'], transcript_table_filtered['gene']))
    trans2chr = dict(zip(transcript_table_filtered['transcript'], transcript_table_filtered['chromosome']))
    trans2order = dict(zip(transcript_table_filtered['transcript'], transcript_table_filtered['gene_order']))

    # 开始生成CDS/Prot序列
    print(f"\033[94mProcessing\033[0m [{args.ingff3}, {args.infna}] >>> [{args.outfna}, {args.outfaa}]")
    for opt, outfile in [("-x", args.outfna), ("-y", args.outfaa)]:
        sp.run(["gffread", args.ingff3, "-g", args.infna, opt, outfile], check=True)

    # 对生成的序列进行筛选
    # 若序列label与transcript库匹配,则取用
    filtered_records_cds = [
        record for record in SeqIO.parse(args.outfna, "fasta") if record.id in list(transcript_table_filtered['transcript'])
    ]
    filtered_records_prot = [
        record for record in SeqIO.parse(args.outfaa, "fasta") if record.id in list(transcript_table_filtered['transcript'])
    ]

    # 将transcript名称改为基因名称
    for idx, record in enumerate(filtered_records_cds):
        record.description = f"index={idx+1},gene={trans2chr[record.id]}g{trans2order[record.id]}"
        record.id = trans2gene[record.id]
    SeqIO.write(filtered_records_cds, args.outfna, "fasta")

    for idx, record in enumerate(filtered_records_prot):
        record.description = f"index={idx+1},gene={trans2chr[record.id]}g{trans2order[record.id]}"
        record.id = trans2gene[record.id]
        record.seq = record.seq.replace(old='.', new="X")
    SeqIO.write(filtered_records_prot, args.outfaa, "fasta")
    
    print("\033[92mPre-Process Step-2 Complete!\033[0m")
    print()

if __name__ == "__main__":
    main()

后记

回来之后,老师主动跟我说了 WGDI 的事情,以及做染色体核心演化的研究方向,于是重新捡起了这篇文章——但我已经忘得一干二净了!

万幸我那时工作留痕,写了这两篇文档,重新捡起了 WGDI,又对文章做了修改与大量补充。

回想起那时的经历——在三个月内恶补 Linux、BioPython 与 JCVI,从 WSL 到 Conda 再到写论文,坐电脑前昏天黑地没个头的日子,忙是真的忙,也非常地疯狂,但也真的有意思,越来越发觉我确实适合这个工作,这就是我应该坚持做的事情。

想到这,因为迷茫、生病而浑浑噩噩度过的本科生涯,也算是没白过了。

2025年8月29日。


之前写的代码多少过于凌乱,特别是对 DataFrame 的处理,甚至直接使用 Column 的索引号,而不是名称。两大坨“屎山”说是。

本着“经常洗洗更健康”的原则,我又对这两个脚本进行了优化,并在多个基因组数据中进行试验。目前已经做到能用,且可读性尚可了。

2025年11月10日。

参考文献