如何使用 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

命令基本上需要:

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