使用bash按列分组并创建一个新的数据框

3
我有一个看起来像这样的数据框:
chr1,A,1,3,y,-
chr1,A,2,30,y,-
chr1,A,12,40,y,-
chr2,B,0,3,y,-
chr2,B,1,30,y,-
chr3,C,1,3,y,-

我想使用bash按照第二列进行分组,然后从第三列取最小值,从第四列取最大值。所以期望的输出应该是:
chr1,A,1,40,y,-
chr2,B,0,30,y,-
chr3,C,1,3,y,-

我成功地构建了一个简陋的代码来实现它,但它并没有完全生成最终的输出。
以下是代码:
awk 'BEGIN{FS=OFS=","} {if (!( $2 in min )) { min[$2] = $3; max[$2] = $4; row[$2] = $0 } else { if ($3 < min[$2]) min[$2] = $3; if ($4 > max[$2]) max[$2] = $4; row[$2] = $0 } } END { for (key in row) print row[key] }'

而我使用这段代码得到的最终输出是:

chr1,A,2,30,y,-
chr2,B,1,30,y,-
chr3,C,1,3,y,-

如何获得我想要的输出?有没有更简单的bash代码可以实现这个功能?谢谢。

1
其他列(例如您示例中的第5列和第6列)是否保证对于所有唯一的第3列值都是相同的?如果不是,您希望打印哪些值 - 第一个、最后一个还是其他什么? - Ed Morton
1
其他列(在您的示例中是第五列和第六列)是否保证对于所有唯一的第三列值都是相同的?如果不是,您想要打印哪些值 - 首个、最后一个还是其他什么? - Ed Morton
1
其他列(例如您示例中的第5列和第6列)是否保证对于所有唯一的第3列值都是相同的?如果不是,您希望打印哪些值 - 第一个、最后一个还是其他什么? - undefined
5个回答

5

使用GNU datamash

$ <ip.txt datamash -t, -g1,2 min 3 max 4 first 5-6
chr1,A,1,40,y,-
chr2,B,0,30,y,-
chr3,C,1,3,y,-
  • -t, 使用,作为字段分隔符
  • -g1,2 按第一列和第二列分组(假设它们始终与示例中显示的相同)
  • min 3 获取第三列的最小值
  • max 4 获取第四列的最大值
  • first 5-6 对于最后两列,只使用第一个实例

1
非常好(并且得到了赞同)。这让我想起我一定要看看datamash - Renaud Pacalet
1
太棒了(并且已经点赞)。这让我想起我一定要去看看datamash - Renaud Pacalet
1
这太棒了,谢谢你介绍datamash - Apex
1
@konsolebox 据我所知,不是的。你可以查看 https://github.com/BurntSushi/xsv 代替。 - Sundeep
1
@konsolebox 据我所知并不是这样,你可以查看 https://github.com/BurntSushi/xsv 代替。 - Sundeep
显示剩余5条评论

2
你的解决方案不起作用,因为你将未修改的行存储在数组row中,并以第二个字段作为键。所以,在END块中打印row的内容只会打印具有该键的最后一行,无论它具有什么第三个和第四个字段,而不是你为该键计算的最小值和最大值。
这里是一个变种,它会不断更新行的第三个和第四个字段,使用当前键的最小值和最大值,然后再将其存储在row数组中。
awk '
BEGIN { FS = OFS = "," }
!($2 in row) { min[$2] = $3; max[$2] = $4; row[$2] = $0; next }
$3 < min[$2] { min[$2] = $3 }
$4 > max[$2] { max[$2] = $4 }
{ $3 = min[$2]; $4 = max[$2]; row[$2] = $0 }
END { for(key in row) print row[key] }' foo.txt

或者,如果原始顺序很重要:

awk '
BEGIN { FS = OFS = "," }
!($2 in row) { key[++n] = $2; min[$2] = $3; max[$2] = $4; row[$2] = $0; next }
$3 < min[$2] { min[$2] = $3 }
$4 > max[$2] { max[$2] = $4 }
{ $3 = min[$2]; $4 = max[$2]; row[$2] = $0 }
END { for(i = 1; i <= n; i++) print row[key[i]] }' foo.txt

注意:如果您的输入已经按照第二个字段进行分组,就像您的示例一样,我们可以使用相同的原则,但是使用标量而不是数组(内存使用更少),并实时打印输出结果(反应更迅速):

awk '
BEGIN { FS = OFS = "," }
NR > 1 && $2 != key { print row }
NR == 1 || $2 != key { key = $2 ; min = $3; max = $4; row = $0; next }
$3 < min { min = $3 }
$4 > max { max = $4 }
{ $3 = min; $4 = max; row = $0 }
END { print row }' foo.txt

1
使用任何awk:
$ cat tst.awk
BEGIN { FS=OFS="," }
$2 != prev { prt(); prev=$2 }
min > $3 { min = $3 }
max < $4 { max = $4 }
END { prt() }

function prt(   orig) {
    if ( NR > 1 ) {
        orig = $0
        $0 = line
        $3 = min
        $4 = max
        print
        $0 = orig
    }
    line = $0
    min  = $3
    max  = $4
}

$ awk -f tst.awk file
chr1,A,1,40,y,-
chr2,B,0,30,y,-
chr3,C,1,3,y,-

Note that unlike the solutions that produce output from a loop in an END block, the above does not store all of the input in memory, just the current set of lines for each unique $2 value at a time, so it'll work for arbitrarily large input files but that means your input does have to be grouped on $2 values as shown in your example. If it isn't then just run sort -t, -k2,2 on it before the awk script.

It'll also produce output lines in the same order as the input lines, if that matters, which would not be guaranteed to be true for any solution that does for (i in whatever) to produce output.


1
假设字段1、2、5和6是唯一的:
awk -F, -v OFS=, '
    { k = $1 FS $2 FS $5 FS $6 }
    !seen[k]++ {
        keys[key_count++] = k
        fields[k, 1] = $1; fields[k, 2] = $2; fields[k, 5] = $5; fields[k, 6] = $6
        min[k] = $3; max[k] = $4
        next
    }
    { min[k] = $3 < min[k] ? $3 : min[k]; max[k] = $4 > max[k] ? $4 : max[k] }
    END {
        for (j = 0; j < key_count; ++j) {
            k = keys[j]
            print fields[k, 1], fields[k, 2], min[k], max[k], fields[k, 5], fields[k, 6]
        }
    }' file

输出:

chr1,A,1,40,y,-
chr2,B,0,30,y,-
chr3,C,1,3,y,-

你的代码存在许多问题,包括没有初始化特定 "key" 的最小值和最大值。


1

使用除了最小/最大值之外的所有字段进行分组,如果数据不一致,输出中会显示而不是忽略。会丢失原始记录顺序,但可以通过对所需字段进行排序来恢复。

awk -F,           ' {k=$1 FS $2 FS SUBSEP FS $5 FS $6} 
        !(k in min) {min[k]=$3; max[k]=$3} 
          $3<min[k] {min[k]=$3} 
          $4>max[k] {max[k]=$4} 
                END {for(k in min) 
                      {sub(SUBSEP, min[k] FS max[k], k); print k}}' file | sort

chr1,A,1,40,y,-                                                                                                                              
chr2,B,0,30,y,-                                                                                                                              
chr3,C,1,3,y,-     

   

如果$3和$4中的值不能包含&amp;,那么这样做是可以的,但如果它们可以包含,那么这个sub(...)将会出现难以理解的错误。 - Ed Morton
如果$3和$4中的值不能包含&,那么这样做就没问题;但如果它们可以包含&,那么这个sub(...)将会出现难以理解的错误。 - Ed Morton
如果$3和$4中的值不能包含&,那么这样做就没问题,但如果它们可以包含&,那么这个sub(...)将会出现难以理解的错误。 - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接