2022-11-09 19:38:20 星期三

目的

平时需要核查二代 DNA 数据检出的胚系变异是否可信,需要反复的打开 igv ,核查变异位置及相关条件,才能判断变异真假程度。此过程重复聒噪,当 igv 加载缓慢时尤为费时,而且人的判断可能带有主观意识,不能保证每次判断结果完全一致,特别面对不太确定变异。

思路

搜集评定变异可信度所需的证据项,给与每个证据项符合逻辑规律的加减分机制,最终得分越高,变异可信度越高,并划分合理的等级。(仿照 ACMG 的 5 等级)

证据项

支持性证据

  • 等位基因深度(AD),测到变异的 reads 越多,变异真实性越高。因此该项赋分应该是正数,应符合一个单调递增函数,且 AD 较小时(AD<=8) 加分比重应该较小,即斜率与 AD 成正比。
  • 等位基因频率(AF),测到变异的 reads 占总体比重越高,变异真实性越高。赋分应是正数,需符合单调递增函数,且 AF 越小时加分比重越小,即斜率与 AF 成正比。
  • 测穿 reads 的对数,测穿变异的 reads 对数越多,变异真实性越高。赋值应该是正数,需符合单调递增函数,且其数量越高,加分比比重越小,即斜率与测穿 reads 的对数成反比。

反对性证据

  • 总的测序深度(DP),该项赋分应该为负数,在 DP 越小,变异越不可靠。DP 应符合一个单调递减函数,且 DP 越小时,比重越高(斜率越大)。
  • 是否是 reads 的唯一比对位置?唯一比对区域就不用考虑是比对造成的假变异,非唯一比对位置应该减分。
  • 是否是复杂序列区域?变异处于简单序列区域(如 A 碱基重复区域)可能是测序错误导致的假变异,应该减分。
  • 是否处于高质量 reads 上?低 MQ reads 上的变异较可能是假变异,应该减分。

代码示例

点击查看代码
import sys
import math
import logging


logging.basicConfig(
    level = logging.DEBUG,
    format = "[%(asctime)s - %(levelname)-4s - %(module)s : line %(lineno)s] %(message)s",
    datefmt = "%Y-%m-%d %H:%M:%S"
)


def DP_score(DP: float) -> float:
    """ DP 深度得分 """
    if DP <= 6:
        return round(-((DP-7) ** 2), 2)
    return 0

def AD_score(AD: int) -> float:
    """ AD 等位基因深度的得分 """
    if AD <= 8:
        return round(AD**2/2)
    return 40

def AF_score(AF: float) -> float:
    """ AF 等位基因频率比率得分, 越大得分越多 """
    if AF < 1:
        return round(AF**2 * 50, 2)
    return 50

def Is_score(Is_reads: int) -> int:
    """ 测穿 reads 对数量得分 """
    if Is_reads <= 5:
        return math.sqrt(Is_reads) * 17
    return 45

def cal_level(score: int) -> str:
    """ 根据得分划分等级 """
    if score < 0:
        return 'E'
    elif 0 <= score < 40:
        return 'D'
    elif 40 <= score < 80:
        return 'C'
    elif 80 <= score < 120:
        return 'B'
    return 'A'

def call_score(
        AD: int,
        AF: float,
        Is_reads: int,
        Uniq_region: bool,
        complex_region: bool,
        high_MQ_reads: bool,
    ) -> float:
    """
        Is_reads : 测穿的 reads 对数
        Uniq_region : 是否为唯一比对区域 
        complex_region : 是否为高复杂度区域 
        high_MQ_reads : 是否在高 MQ 的 reads 上,变异所在 reads 的平均 MQ 
            不低于变异位置所有 reads 的平均 MQ 的 50%
    """
    # 数据检查
    if AF < 0 or AF > 1:
        logging.error(f'AF 不在 (0,1] 范围内!')
        sys.exit(1)
    if Is_reads*2 > AD:
        logging.error(f'测穿变异的 reads 数量比 AD 要多:\
            \n\tAD:{AD}   AF:{AF}   测穿 reads 对数:{Is_reads}')
        sys.exit(1)
    # 总深度 DP
    score = 0
    score += DP_score(AD/AF)
    score += AD_score(AD)
    score += AF_score(AF)
    score += Is_score(Is_reads)
    if not Uniq_region:
        score -= 30
    if not complex_region:
        score -= 30
    if not high_MQ_reads:
        score -= 30
    # 三相不满足,多减,两项不满足,多减
    if not Uniq_region and not high_MQ_reads and not complex_region:
        score -= 20
    elif (not Uniq_region and not high_MQ_reads) or \
        (not high_MQ_reads and not complex_region) or \
        (not Uniq_region and not complex_region):
        score -= 10
    if Uniq_region and complex_region and high_MQ_reads:
        score += 10
    return (round(score, 2))

结果解读

cal_level 函数获得的 A-E 的评级,分别表示:

  • A: 可信的变异
  • B: 可能是真的变异
  • C: 不确定真实性的变异
  • D: 可能是假的变异
  • E: 假的变异

模拟数据验证

采用造数据的形式,模拟出低深度(DP<30)时,该评分机制评级效果。(代码基本在 2s 左右运行完成,共有 20560 种可能的情况)

代码

点击查看代码
with open('res.tsv', 'w') as fo:
    fo.write('Uniq\tcomplex\thigh_MQ\tDP\tAD\tAF\tIs_reads\tScore\tLevel\n')
    res = ''
    for DP in range(1, 30):
        for Uniq_region in [0,1]:
            for complex_region in [0, 1]:
                for high_MQ_reads in [0, 1]:
                    for AD in range(1, DP+1):
                        if AD > DP:
                            continue
                        AF = round(AD / DP,2)
                        for Is_reads in range(0, (AD // 2) + 1):
                            score = call_score(AD, AF, Is_reads, Uniq_region, complex_region, high_MQ_reads)
                            level = cal_level(score)
                            res = f"{res}{Uniq_region}\t{complex_region}\t{high_MQ_reads}\t{DP}\t{AD}\t{AF}\t{Is_reads}\t{score}\t{level}\n"
    fo.write(res)

结果示例

由于结果行数太多,这里仅列举一些比较有代表性的情况供参考:

点击查看代码
Uniq   complex   high_MQ   DP   AD   AF     Is_reads   Score     Level
----   -------   -------   --   --   ----   --------   -------   -----
0      0         0         1    1    1.0    0          -96.0     E
0      0         1         1    1    1.0    0          -56.0     E
0      1         0         1    1    1.0    0          -56.0     E
0      1         1         1    1    1.0    0          -16.0     E
1      0         0         1    1    1.0    0          -56.0     E
1      0         1         1    1    1.0    0          -16.0     E
1      1         0         1    1    1.0    0          -16.0     E
1      1         1         1    1    1.0    0          24.0      D
0      0         0         2    1    0.5    0          -122.5    E
0      0         0         2    2    1.0    0          -83.0     E
1      1         1         6    4    0.67   2          63.43     C
1      1         1         6    5    0.83   0          56.45     C
1      1         1         6    5    0.83   1          73.45     C
1      1         1         6    5    0.83   2          80.49     B
1      1         1         6    6    1.0    0          77.0      C
1      1         1         6    6    1.0    1          94.0      B
1      1         1         6    6    1.0    2          101.04    B
1      1         1         6    6    1.0    3          106.44    B
1      1         1         8    7    0.88   1          89.72     B
1      1         1         8    7    0.88   2          96.76     B
1      1         1         8    7    0.88   3          102.16    B
1      1         1         8    8    1.0    0          92.0      B
1      1         1         8    8    1.0    1          109.0     B
1      1         1         8    8    1.0    2          116.04    B
1      1         1         8    8    1.0    3          121.44    A
1      1         1         8    8    1.0    4          126.0     A

拓展验证

对于这类评定机制没有标准,需要反复摸索条件、调试各证据项的评分规律以及后续分级阈值的程序,必然需要一些比较规范的验证机制。(与代码的 test 模块类似)
比如,有一些证据项情况,我们希望在调整完任何一部分的赋分或者分级阈值时,依然如预期的工作着。在代码 opts 中添加所需参数和预期结果,从而可以批量核查指定情况下,程序是否按预期得到了结果。

点击查看代码
def all_check_group():
    """ 所有需要的数据核查 """
    opts = [
        {
            'func' : call_score,     # 需要运行的函数
            'args' : {               # 函数的参数
                'AD': 10, 
                'AF': 1, 
                'Is_reads': 5, 
                'Uniq_region': 1, 
                'complex_region': 1,
                'high_MQ_reads': 1,
            },
            'res' : 'A',             # 预期结果
            'call_back' : cal_level  # func 函数运行后的结果调用的函数
        },
        {
            'func' : call_score,
            'args' : {
                'AD': 6, 
                'AF': 1, 
                'Is_reads': 3, 
                'Uniq_region': 1, 
                'complex_region': 1,
                'high_MQ_reads': 1,
            },
            'res' : 'B', 
            'call_back' : cal_level
        },
    ]
    pass_test_num = 0
    for k in opts:
        if 'call_back' in k and k['call_back']:
            res = k['call_back'](k['func'](**k['args']))
            if res != k['res']:
                logging.warning(f"函数 \n\t{k['call_back'].__name__}({k['func'].__name__}(**{k['args']})) \
                    \n\t结果与预期不符:\n\t预期: {k['res']}\n\t实际: {res}\n")
            else:
                pass_test_num += 1
            continue
        res = k['func'](k['args'])
        if res != k['res']:
            logging.warning(f"函数 {k['func'].__name__}({k['args']}) \
                结果与预期不符:\n\t预期: {k['res']}\n\t实际: {res}\n")
        else:
            pass_test_num += 1
    logging.info(f'共 {len(opts)} 个测试, 成功 {pass_test_num} 个, 失败 {len(opts) - pass_test_num} 个.')

总结

该随笔及其部分代码仅仅是一个思路的描述,可拓展的地方很多,比如更全面的证据项,变异的分级、评级机制等都可商榷评估,以使其更为合理。另外该部分不包含这些证据项如何从 bam 中提取出来,后续的工作还有很多,继续加油干吧~~