计算DNA序列的编辑距离(Levenshtein Distance)
引言
在生物信息学和计算机科学中,编辑距离(Levenshtein Distance)是一个重要的概念,用于衡量两个字符串之间的相似度。编辑距离是指将一个字符串转换为另一个字符串所需的最少单字符编辑操作次数。本文将详细解析一段Python代码,该代码使用动态规划(Dynamic Programming)方法计算两个DNA序列之间的编辑距离。
问题描述
给定两个DNA序列 dna1 和 dna2,我们需要计算将 dna1 转换为 dna2 所需的最少编辑步骤。编辑步骤包括增加一个碱基、删除一个碱基或替换一个碱基。
动态规划方法
动态规划是一种通过将复杂问题分解为子问题来解决的方法。对于编辑距离问题,我们可以使用动态规划来逐步计算每个子问题的最优解,最终得到整个问题的最优解。
1. 定义状态
设 dp[i][j] 表示将 dna1 的前 i 个字符转换为 dna2 的前 j 个字符所需的最少编辑步骤数。
2. 初始化
dp[0][j] 表示将空字符串转换为 dna2 的前 j 个字符,需要 j 次插入操作。
dp[i][0] 表示将 dna1 的前 i 个字符转换为空字符串,需要 i 次删除操作。
3. 状态转移方程
如果 dna1[i-1] == dna2[j-1],则 dp[i][j] = dp[i-1][j-1](不需要编辑操作)。
否则,dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1(分别对应删除、插入和替换操作)。
代码
python
# https://juejin.cn/post/7437633158910394368
# 计算编辑距离
import functools
def log_function_call(func=None, *, prefix="Calling function: "):
"""
装饰器:在每次函数调用时打印函数名
:param prefix: 自定义前缀文本
"""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"{prefix}{func.__name__}")
return func(*args, **kwargs)
return wrapper
# 支持带参数和不带参数两种调用方式
if func is None:
return decorator
else:
return decorator(func)
def solution(dna1, dna2, echo=False):
m, n = len(dna1), len(dna2)
# 创建一个 (m+1) x (n+1) 的二维数组
dp = [[0] * (n + 1) for _ in range(m + 1)]
# 初始化第一行和第一列
for i in range(m + 1):
dp[i][0] = i
for j in range(n + 1):
dp[0][j] = j
# 填充dp数组
for i in range(1, m + 1):
for j in range(1, n + 1):
if dna1[i - 1] == dna2[j - 1]:
dp[i][j] = dp[i - 1][j - 1]
else:
dp[i][j] = min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]) + 1
if echo:
for i in dp:
i = [str(j) for j in i]
print("\t".join(i))
# 返回最终结果
return dp[m][n]
@log_function_call
def read_col_ref(ref_file, col):
seq_list = []
with open(ref_file) as f:
next(f)
for line in f:
seq = line.strip().split("\t")[col-1]
seq_list.append(seq)
return seq_list
@log_function_call
def matrix_distance(seq_list, out_file):
f_out = open(out_file, "w")
header = "\t".join([f"InA-{i}" for i in list(range(1, len(seq_list)+1))])
print("\t" + header, file=f_out)
for i in range(1, len(seq_list)+1):
line_distance_list = []
seq_1 = seq_list[i-1]
for j in range(1, len(seq_list)+1):
seq_2 = seq_list[j-1]
distance = solution(seq_1, seq_2)
line_distance_list.append(str(distance))
print(f"InA-{i}\t" + '\t'.join(line_distance_list), file=f_out)
f_out.close()
def reverse_complement(sequence):
reverse_seq = sequence[::-1]
complement_table = str.maketrans('ATGC', 'TACG')
complement_seq = reverse_seq.translate(complement_table)
return complement_seq
@log_function_call
def reverse_complement_file(seq_list, out_file):
f_out = open(out_file, "w")
print("反向互补", file=f_out)
for seq in seq_list:
reverse_complement_seq = reverse_complement(seq)
print(reverse_complement_seq, file=f_out)
f_out.close()
@log_function_call
def merge_file(file_1, file_2, file_3, out_file):
with open(out_file, "w") as f_out:
with open(file_1) as f_1, open(file_2) as f_2, open(file_3) as f_3:
f_1_list = f_1.readlines()
f_2_list = f_2.readlines()
f_3_list = f_3.readlines()
for idx in range(len(f_1_list)):
col_1 = f_1_list[idx].replace("\n", "\t")
col_2 = f_2_list[idx].replace("\n", "\t")
col_3 = f_3_list[idx].replace("\n", "\t")
f_out.write(col_1 + col_2 + col_3 + "\n")
if __name__ == "__main__":
# 输入
ref_file = "barcode_contamination/levenshtein.txt"
# 输出
distance_out_file = "barcode_contamination/levenshtein_distance.txt"
reverse_complement_out_file = "reverse_complement.txt"
# 最终结果
final_out_file = "levenshtein_distance_reverse_complement.xls"
seq_list = read_col_ref(ref_file, 3)
matrix_distance(seq_list, distance_out_file)
reverse_complement_file(seq_list, reverse_complement_out_file)
merge_file(ref_file, reverse_complement_out_file, distance_out_file, final_out_file)
exit()
dna1 = "TCATCAGCAAACGCAGAATCAGCGGTATGGCTCTTCTCATATTGGCGCTA"
dna2 = "ATCAGCAAACGCAGAATCAGCGGTATGGCTCTTCTCATATTGGCGCTACT"
# dna1 = "AGCTTAGC"
# dna2 = "AGCTAGCT"
solution(dna1, dna2, echo=True)
参考: