如何使用 bash 中的 awk 删除具有相似数据的行以仅保留特定列(tsv 文件)中的最高值?
How to remove rows with similar data to keep only highest value in a specific column (tsv file) with awk in bash?
我有一个非常大的 .tsv
文件 (80 GB) 需要编辑。它由 5 列组成。最后一列代表分数。有些位置有多个“分数”条目,我只需要保留每个位置的最高值所在的行。
例如,这个位置每个组合有多个条目:
1 861265 C A 0.071
1 861265 C A 0.148
1 861265 C G 0.001
1 861265 C G 0.108
1 861265 C T 0
1 861265 C T 0.216
2 193456 G A 0.006
2 193456 G A 0.094
2 193456 G C 0.011
2 193456 G C 0.152
2 193456 G T 0.003
2 193456 G T 0.056
所需的输出如下所示:
1 861265 C A 0.148
1 861265 C G 0.108
1 861265 C T 0.216
2 193456 G A 0.094
2 193456 G C 0.152
2 193456 G T 0.056
无法在 python/pandas 中执行此操作,因为文件太大或耗时太长。因此,我正在寻找使用 bash
的解决方案;特别是 awk
.
已使用以下命令对 Thif 输入文件进行排序:
sort -t$'\t' -k1 -n -o sorted_file original_file
命令基本上需要:
- 比较
sorted_file
中前 4 列的数据
- 如果所有这些都相同,则只有第 5 列具有最高值的行应该打印到输出文件中。
我对
awk
语法不是很熟悉。我在其他论坛上看到过类似的问题,但我无法根据我的具体情况进行调整。我试图将其中一种解决方案适应我的情况,如下所示:
awk -F, 'NR==1 {print; next} NR==2 {key=; next} != key {print lastval; key = } {lastval = [=14=]} END {print lastval}' sorted_files.tsv > filtered_file.tsv
但是,输出文件看起来根本不像它应该的样子。
非常感谢任何帮助。
已更新
摘自 sort
手册:
-k, --key=KEYDEF
KEYDEF is F[.C][OPTS][,F[.C][OPTS]]
for start and stop position, where F is a field number and C a character position in the field; both are origin 1, and the stop position defaults to the line's end.
这意味着像你一样使用sort -t$'\t' -k1 -n
,文件的所有字段都对数值排序有贡献。
这可能是 最快 awk
解决方案,它利用数字升序排序:
awk '
BEGIN {
FS = "\t"
if ((getline line) > 0) {
split(line, arr)
prev_key = arr[1] FS arr[2] FS arr[4]
prev_line = [=10=]
}
}
{
curr_key = FS FS
if (curr_key != prev_key) {
print prev_line
prev_key = curr_key
}
prev_line = [=10=]
}
END {
if (prev_key) print prev_line
}
' file.tsv
注意:当您处理一个大约有 40 亿行的文件时,我试图将操作数保持在最低限度。例如:
- 只需将
FS
设置为 "\t"
,即可节省 800 亿次 操作。事实上,当您处理 TSV 时,为什么允许 awk
将文件的每个字符与 " "
进行比较?
- 通过处理
BEGIN
块中带有 getline
的第一行,节省了 40 亿次 比较。有些人可能会说使用 (NR == 1)
and/or (NR > 1)
是 safer/better/cleaner,但这意味着对每个输入行进行 2 次比较而不是 0 次比较。
可能值得将此代码的执行时间与@EdMorton 的 进行比较,后者使用相同的算法但未进行这些优化。尽管 ^^
磁盘速度可能会拉平差异
Assumptions/Understandings:
- 文件按第一个字段排序
- 不保证字段 #2、#3 和 #4 的顺序
- 必须保持当前的行顺序(这似乎排除了(重新)排序文件,因为我们可能会丢失当前的行顺序)
- 给定
group
的完整输出行集将适合内存(又名 awk
数组)
总体规划:
- 我们将字段 #1 称为
group
字段;在字段 #1 中具有相同值的所有行都被视为相同 group
的一部分
- 对于给定的
group
我们通过 awk
数组 arr[]
跟踪所有输出行(索引将是字段 #2、#3、#4 的组合)
- 我们还通过
awk
数组 order[]
跟踪传入的行顺序
- 更新
arr[]
如果我们在字段 #5 中看到一个值高于之前的值
- 当
group
更改时将 arr[]
索引的当前内容刷新到标准输出
一个awk
想法:
awk '
function flush() { # function to flush current group to stdout
for (i=1; i<=seq; i++)
print group,order[i],arr[order[i]]
delete arr # reset arrays
delete order
seq=0 # reset index for order[] array
}
BEGIN { FS=OFS="\t" }
!=group { flush()
group=
}
{ key= OFS OFS
if ( key in arr && <= arr[key] )
next
if ( ! (key in arr) )
order[++seq]=key
arr[key]=
}
END { flush() } # flush last group to stdout
' input.dat
这会生成:
1 861265 C A 0.148
1 861265 C G 0.108
1 861265 C T 0.216
2 193456 G A 0.094
2 193456 G C 0.152
2 193456 G T 0.056
你可以试试这个方法。这也适用于未排序的最后一列,只需对前 4 列进行排序。
% awk 'NR>1&&str!=" "" "" "{print line; m=0}
>=m{m=; line=[=10=]}
{str=" "" "" "} END{print line}' file
1 861265 C A 0.148
1 861265 C G 0.108
1 861265 C T 0.216
2 193456 G A 0.094
2 193456 G C 0.152
2 193456 G T 0.056
数据
% cat file
1 861265 C A 0.071
1 861265 C A 0.148
1 861265 C G 0.001
1 861265 C G 0.108
1 861265 C T 0
1 861265 C T 0.216
2 193456 G A 0.006
2 193456 G A 0.094
2 193456 G C 0.011
2 193456 G C 0.152
2 193456 G T 0.003
2 193456 G T 0.056
假设您的实际输入是按键排序,然后按照与您的示例相同的方式对值进行升序排序:
$ cat tst.awk
{ key = FS FS FS }
key != prevKey {
if ( NR > 1 ) {
print prevRec
}
prevKey = key
}
{ prevRec = [=10=] }
END {
print prevRec
}
$ awk -f tst.awk file
1 861265 C A 0.148
1 861265 C G 0.108
1 861265 C T 0.216
2 193456 G A 0.094
2 193456 G C 0.152
2 193456 G T 0.056
如果您的数据尚未排序,则只需将其排序为:
sort file | awk ..
只有 sort
这样才能一次处理整个文件,它的设计是通过使用请求分页等来实现的,因此 运行 内存不足的可能性要小得多如果您将整个文件读入 awk 或 python 或任何其他工具
使用 sort 和 awk:
sort -t$'\t' -k1,1n -k4,4 -k5,5rn file | awk 'BEGIN{FS=OFS="\t"} !seen[,]++'
打印:
1 861265 C A 0.148
1 861265 C G 0.108
1 861265 C T 0.216
2 193456 G A 0.094
2 193456 G C 0.152
2 193456 G T 0.056
这假设 'group' 定义为第 1 列。
首先按第 1 列分组,然后按第 4 列(每个字母)分组,然后对第 5 列进行反向数字排序。
然后 awk 打印第一组字母,这将是基于排序的最大值。
一种更可靠的方法是按数字对最后一个字段进行排序,然后让 awk
选择第一个值。如果您的字段没有空格,则无需指定分隔符。
$ sort -k1n k5,5nr original_file | awk '!a[,,,]++' > max_value_file
正如@Fravadona 评论的那样,由于这存储了键,如果有许多唯一记录,它将占用大量内存。一个替代方案是委托 uniq
从重复的条目中挑选第一条记录。
$ sort -k1n k5,5nr original_file |
awk '{print ,,,,}' |
uniq -f1 |
awk '{print ,,,,}'
我们更改字段的顺序以跳过要比较的值,然后再改回来。这不会有任何内存占用(除了 sort
,它将被管理)。
如果您不是纯粹主义者,这应该与上一个相同
$ sort -k1n k5,5nr original_file | rev | uniq -f1 | rev
不是awk,但是使用Miller,非常简单有趣
mlr --tsv -N sort -f 1,2,3,4 -n 5 then top -f 5 -g 1,2,3,4 -a input.tsv >output.tsv
你将拥有
1 861265 C A 1 0.148
1 861265 C G 1 0.108
1 861265 C T 1 0.216
2 193456 G A 1 0.094
2 193456 G C 1 0.152
2 193456 G T 1 0.056
我有一个非常大的 .tsv
文件 (80 GB) 需要编辑。它由 5 列组成。最后一列代表分数。有些位置有多个“分数”条目,我只需要保留每个位置的最高值所在的行。
例如,这个位置每个组合有多个条目:
1 861265 C A 0.071
1 861265 C A 0.148
1 861265 C G 0.001
1 861265 C G 0.108
1 861265 C T 0
1 861265 C T 0.216
2 193456 G A 0.006
2 193456 G A 0.094
2 193456 G C 0.011
2 193456 G C 0.152
2 193456 G T 0.003
2 193456 G T 0.056
所需的输出如下所示:
1 861265 C A 0.148
1 861265 C G 0.108
1 861265 C T 0.216
2 193456 G A 0.094
2 193456 G C 0.152
2 193456 G T 0.056
无法在 python/pandas 中执行此操作,因为文件太大或耗时太长。因此,我正在寻找使用 bash
的解决方案;特别是 awk
.
已使用以下命令对 Thif 输入文件进行排序:
sort -t$'\t' -k1 -n -o sorted_file original_file
命令基本上需要:
- 比较
sorted_file
中前 4 列的数据
- 如果所有这些都相同,则只有第 5 列具有最高值的行应该打印到输出文件中。
我对
awk
语法不是很熟悉。我在其他论坛上看到过类似的问题,但我无法根据我的具体情况进行调整。我试图将其中一种解决方案适应我的情况,如下所示:
awk -F, 'NR==1 {print; next} NR==2 {key=; next} != key {print lastval; key = } {lastval = [=14=]} END {print lastval}' sorted_files.tsv > filtered_file.tsv
但是,输出文件看起来根本不像它应该的样子。 非常感谢任何帮助。
已更新
摘自 sort
手册:
-k, --key=KEYDEF
KEYDEF isF[.C][OPTS][,F[.C][OPTS]]
for start and stop position, where F is a field number and C a character position in the field; both are origin 1, and the stop position defaults to the line's end.
这意味着像你一样使用sort -t$'\t' -k1 -n
,文件的所有字段都对数值排序有贡献。
这可能是 最快 awk
解决方案,它利用数字升序排序:
awk '
BEGIN {
FS = "\t"
if ((getline line) > 0) {
split(line, arr)
prev_key = arr[1] FS arr[2] FS arr[4]
prev_line = [=10=]
}
}
{
curr_key = FS FS
if (curr_key != prev_key) {
print prev_line
prev_key = curr_key
}
prev_line = [=10=]
}
END {
if (prev_key) print prev_line
}
' file.tsv
注意:当您处理一个大约有 40 亿行的文件时,我试图将操作数保持在最低限度。例如:
- 只需将
FS
设置为"\t"
,即可节省 800 亿次 操作。事实上,当您处理 TSV 时,为什么允许awk
将文件的每个字符与" "
进行比较? - 通过处理
BEGIN
块中带有getline
的第一行,节省了 40 亿次 比较。有些人可能会说使用(NR == 1)
and/or(NR > 1)
是 safer/better/cleaner,但这意味着对每个输入行进行 2 次比较而不是 0 次比较。
可能值得将此代码的执行时间与@EdMorton 的
Assumptions/Understandings:
- 文件按第一个字段排序
- 不保证字段 #2、#3 和 #4 的顺序
- 必须保持当前的行顺序(这似乎排除了(重新)排序文件,因为我们可能会丢失当前的行顺序)
- 给定
group
的完整输出行集将适合内存(又名awk
数组)
总体规划:
- 我们将字段 #1 称为
group
字段;在字段 #1 中具有相同值的所有行都被视为相同group
的一部分
- 对于给定的
group
我们通过awk
数组arr[]
跟踪所有输出行(索引将是字段 #2、#3、#4 的组合) - 我们还通过
awk
数组order[]
跟踪传入的行顺序
- 更新
arr[]
如果我们在字段 #5 中看到一个值高于之前的值 - 当
group
更改时将arr[]
索引的当前内容刷新到标准输出
一个awk
想法:
awk '
function flush() { # function to flush current group to stdout
for (i=1; i<=seq; i++)
print group,order[i],arr[order[i]]
delete arr # reset arrays
delete order
seq=0 # reset index for order[] array
}
BEGIN { FS=OFS="\t" }
!=group { flush()
group=
}
{ key= OFS OFS
if ( key in arr && <= arr[key] )
next
if ( ! (key in arr) )
order[++seq]=key
arr[key]=
}
END { flush() } # flush last group to stdout
' input.dat
这会生成:
1 861265 C A 0.148
1 861265 C G 0.108
1 861265 C T 0.216
2 193456 G A 0.094
2 193456 G C 0.152
2 193456 G T 0.056
你可以试试这个方法。这也适用于未排序的最后一列,只需对前 4 列进行排序。
% awk 'NR>1&&str!=" "" "" "{print line; m=0}
>=m{m=; line=[=10=]}
{str=" "" "" "} END{print line}' file
1 861265 C A 0.148
1 861265 C G 0.108
1 861265 C T 0.216
2 193456 G A 0.094
2 193456 G C 0.152
2 193456 G T 0.056
数据
% cat file
1 861265 C A 0.071
1 861265 C A 0.148
1 861265 C G 0.001
1 861265 C G 0.108
1 861265 C T 0
1 861265 C T 0.216
2 193456 G A 0.006
2 193456 G A 0.094
2 193456 G C 0.011
2 193456 G C 0.152
2 193456 G T 0.003
2 193456 G T 0.056
假设您的实际输入是按键排序,然后按照与您的示例相同的方式对值进行升序排序:
$ cat tst.awk
{ key = FS FS FS }
key != prevKey {
if ( NR > 1 ) {
print prevRec
}
prevKey = key
}
{ prevRec = [=10=] }
END {
print prevRec
}
$ awk -f tst.awk file
1 861265 C A 0.148
1 861265 C G 0.108
1 861265 C T 0.216
2 193456 G A 0.094
2 193456 G C 0.152
2 193456 G T 0.056
如果您的数据尚未排序,则只需将其排序为:
sort file | awk ..
只有 sort
这样才能一次处理整个文件,它的设计是通过使用请求分页等来实现的,因此 运行 内存不足的可能性要小得多如果您将整个文件读入 awk 或 python 或任何其他工具
使用 sort 和 awk:
sort -t$'\t' -k1,1n -k4,4 -k5,5rn file | awk 'BEGIN{FS=OFS="\t"} !seen[,]++'
打印:
1 861265 C A 0.148
1 861265 C G 0.108
1 861265 C T 0.216
2 193456 G A 0.094
2 193456 G C 0.152
2 193456 G T 0.056
这假设 'group' 定义为第 1 列。
首先按第 1 列分组,然后按第 4 列(每个字母)分组,然后对第 5 列进行反向数字排序。
然后 awk 打印第一组字母,这将是基于排序的最大值。
一种更可靠的方法是按数字对最后一个字段进行排序,然后让 awk
选择第一个值。如果您的字段没有空格,则无需指定分隔符。
$ sort -k1n k5,5nr original_file | awk '!a[,,,]++' > max_value_file
正如@Fravadona 评论的那样,由于这存储了键,如果有许多唯一记录,它将占用大量内存。一个替代方案是委托 uniq
从重复的条目中挑选第一条记录。
$ sort -k1n k5,5nr original_file |
awk '{print ,,,,}' |
uniq -f1 |
awk '{print ,,,,}'
我们更改字段的顺序以跳过要比较的值,然后再改回来。这不会有任何内存占用(除了 sort
,它将被管理)。
如果您不是纯粹主义者,这应该与上一个相同
$ sort -k1n k5,5nr original_file | rev | uniq -f1 | rev
不是awk,但是使用Miller,非常简单有趣
mlr --tsv -N sort -f 1,2,3,4 -n 5 then top -f 5 -g 1,2,3,4 -a input.tsv >output.tsv
你将拥有
1 861265 C A 1 0.148
1 861265 C G 1 0.108
1 861265 C T 1 0.216
2 193456 G A 1 0.094
2 193456 G C 1 0.152
2 193456 G T 1 0.056