使用第二个文件中的列名称获取第一个文件中的子集列

使用第二个文件中的列名称获取第一个文件中的子集列

我有两个文本文件:第一个文件是制表符分隔的文件,如下所示:

chrom   pos ref alt a1  a2  a3  a4
10  12345   C   T   aa  bb  cc  dd
10  12345   C   T   aa  bb  cc  dd
10  12345   C   T   aa  bb  cc  dd
10  12345   C   T   aa  bb  cc  dd
10  12345   C   T   aa  bb  cc  dd
10  12345   C   T   aa  bb  cc  dd

第二个文件如下所示:

a1
a4

我想提取第一个文件中第二个文件中存在的那些列以及第一个文件的前 4 列。因此,在上述情况下,输出将如下所示:

chrom   pos ref alt a1  a4
10  12345   C   T   aa  dd
10  12345   C   T   aa  dd
10  12345   C   T   aa  dd
10  12345   C   T   aa  dd
10  12345   C   T   aa  dd
10  12345   C   T   aa  dd

我想在 shell 中执行此操作。我怎样才能做到这一点?我的文件比此处显示的要大,因此第一个文件中有很多列

cut -f 1-4,$(grep -Fwf file2.txt <(head -1 file1.txt)) file1.txt

答案1

如果perl没问题的话:

$ perl -F'\t' -lane 'if(!$#ARGV){ $h{$_}=1; close ARGV if eof; next }
                     @i = grep { exists $h{$F[$_]} } 4..$#F if $.==1;
                     print join "\t", @F[0..3, @i]' f2.txt f1.tsv
chrom   pos ref alt a1  a4
10  12345   C   T   aa  dd
10  12345   C   T   aa  dd
10  12345   C   T   aa  dd
10  12345   C   T   aa  dd
10  12345   C   T   aa  dd
10  12345   C   T   aa  dd

哈希变量将使用第二个文件中的行作为键。

然后,grep用于通过根据哈希键测试字段名称来从 TSV 文件的标题行获取索引。

最后,前 4 列和过滤后的索引值用于打印。

答案2

使用标准命令,我们可以在第二个文件(此处称为)paste中创建以逗号分隔的字段名称列表:file2

$ paste -s -d , - <file2
a1,a4

我们可以使用它米勒 ( mlr),一个用于处理结构化文档(例如 TSV 文件)的实用程序,及其从第一个文件(此处称为)中cut提取前四个字段以及中的字段的子命令:file2file1

$ mlr --tsv cut -f "chrom,pos,ref,alt,$(paste -s -d , - <file2)" file1
chrom   pos     ref     alt     a1      a4
10      12345   C       T       aa      dd
10      12345   C       T       aa      dd
10      12345   C       T       aa      dd
10      12345   C       T       aa      dd
10      12345   C       T       aa      dd
10      12345   C       T       aa      dd

Miller 的子命令cut允许您使用字段名称来选择要提取的字段,这里我们按名称以及文件中的名称选择前四个字段file2

由于该cut子命令还接受以逗号结尾的字段名称列表,因此您可以选择使用它tr '\n' ',' <file2来代替paste前面提到的命令。


如果我们已经获得了想要保留的字段索引,例如file3像这样的文件:

5
8

...然后我们可以使用cut如下标准命令来提取所需的数据:

$ cut -f "1-4,$(paste -s -d , - <file3)" file1
chrom   pos     ref     alt     a1      a4
10      12345   C       T       aa      dd
10      12345   C       T       aa      dd
10      12345   C       T       aa      dd
10      12345   C       T       aa      dd
10      12345   C       T       aa      dd
10      12345   C       T       aa      dd

我们可以使用有点复杂的管道从原始文件创建数字字段索引列表:

$ head -n 1 file1 | tr '\t' '\n' | grep -xF -f file2 -n | cut -d : -f 1 | paste -s -d , -
5,8

这会从数据中提取标题行,并用换行符替换制表符,以使每个字段名称位于单独的行上。然后grep输出与第二个文件中的字段名称相对应的行号。这些输出在表单 上5:a1,其中 之前的数字:是行号,末尾的文本是匹配的字段名称。

使用 隔离该数字cut,然后paste使用该命令将所有字段索引放入逗号分隔的列表中。

因此,完整的命令模拟了mlr这个答案顶部的命令的作用,看起来像这样:

cut -f "1-4,$(
    head -n 1 file1 | tr '\t' '\n' |
    grep -xF -f file2 -n | cut -d : -f 1 |
    paste -s -d , -
)" file1

答案3

使用任何 awk:

$ cat tst.awk
BEGIN { FS=OFS="\t" }
NR == FNR {
    a[$1]
    next
}
FNR == 1 {
    for ( inFldNr=1; inFldNr<=NF; inFldNr++ ) {
        if ( (inFldNr <= 4) || ($inFldNr in a) ) {
            out2in[++numOutFlds] = inFldNr
        }
    }
}
{
    for ( outFldNr=1; outFldNr<=numOutFlds; outFldNr++ ) {
        inFldNr = out2in[outFldNr]
        printf "%s%s", $inFldNr, (outFldNr<numOutFlds ? OFS : ORS)
    }
}

$ awk -f tst.awk file2 file1
chrom   pos     ref     alt     a1      a4
10      12345   C       T       aa      dd
10      12345   C       T       aa      dd
10      12345   C       T       aa      dd
10      12345   C       T       aa      dd
10      12345   C       T       aa      dd
10      12345   C       T       aa      dd

请注意,为了提高效率,读取file1上述每一行时仅循环与要打印的输出字段数相同的次数,因此如果您的输入有 1000 个字段,但您只想打印 10 个字段,则每行将迭代 10 次,而不是超过 1000。

答案4

使用(以前称为 Perl_6)

~$ raku -e 'my ($header,@a) = lines.map: *.split(/ \s+ /); 
            $header .= list;  my @ind = <a1 a4>; 
            my @col = (0...3, $header.grep( / @ind /, :k ).Slip); 
            put $header[@col].join("\t"); 
            say $_.join("\t") for @a.map: *.[@col];'  data.csv

上面是用 Raku(Perl 编程语言家族的成员)编写的答案。与 Perl 一样,Raku 非常适合承担生物信息学等领域的文本整理工作。

首先,使用 Raku 的例程读取数据lines。这些行split位于空白处。将数据保存到$header标量和@a数组中,Raku 知道它$header占据第一行,下一条语句$header从轻量级升级Seqlist.现在,我们内联获取所需的列名称,并将它们存储在@ind数组中。

我们grep提供所需列名称的标题,返回:k它们的索引位置(而不是值)。然后@col使用所需的前四列创建一个数组0...3,以及grep结果,Slip将其组合在一起(即展平)以创建一个简单的索引。

如果我们处理行,我们将使用@array[@index]习惯用法(适用于单行标题)。因为我们正在处理列,所以我们需要map进入数组元素,例如@a.map: *.[@col]。最后,数据join在选项卡上重新显示并输出put

输入示例:

chrom   pos ref alt a1  a2  a3  a4
10  12345   C   T   aa  bb  cc  dd
10  12345   C   T   aa  bb  cc  dd
10  12345   C   T   aa  bb  cc  dd
10  12345   C   T   aa  bb  cc  dd
10  12345   C   T   aa  bb  cc  dd
10  12345   C   T   aa  bb  cc  dd

示例输出:

chrom   pos ref alt a1  a4
10  12345   C   T   aa  dd
10  12345   C   T   aa  dd
10  12345   C   T   aa  dd
10  12345   C   T   aa  dd
10  12345   C   T   aa  dd
10  12345   C   T   aa  dd

升级代码以处理两个文件很简单。只需使用路径来创建@ind索引,这可以帮助您从概念上将列索引文件与数据文件分开:

my @ind = "/path/to/index.csv".IO.lines;

如果您确实更喜欢从命令行获取数据,Raku 可以提供@*ARGS相应的数组。使用 Raku 的IO例程并确保按照您想要的顺序输入文件:

~$ raku -e 'my ($header,@a) = @*ARGS[0].IO.lines.map: *.split(/ \s+ /);
            $header .= list;  my @ind = @*ARGS[1].IO.lines;
            my @col = (0...3, $header.grep( / @ind /, :k ).Slip);
            put $header[@col].join("\t"); 
            say $_.join("\t") for @a.map: *.[@col];'   data.csv  index.csv
chrom   pos ref alt a1  a4
10  12345   C   T   aa  dd
10  12345   C   T   aa  dd
10  12345   C   T   aa  dd
10  12345   C   T   aa  dd
10  12345   C   T   aa  dd
10  12345   C   T   aa  dd

https://docs.raku.org/routine/grep
https://raku.org

相关内容