Unix如何按其关联的最大值对组进行排序?

4

假设我有这个 输入 文件 49142202.txt:

A   5
B   6
C   3
A   4
B   2
C   1

是否可以按照第2列的值对第1列中的组进行排序?期望的输出如下:

B   6 <-- B group at the top, because 6 is larger than 5 and 3
B   2 <-- 2 less than 6
A   5 <-- A group in the middle, because 5 is smaller than 6 and larger than 3
A   4 <-- 4 less than 5
C   3 <-- C group at the bottom, because 3 is smaller than 6 and 5
C   1 <-- 1 less than 3

这是我的解决方案

join -t$'\t' -1 2 -2 1 \
 <(cat 49142202.txt | sort -k2nr,2 | sort --stable -k1,1 -u | sort -k2nr,2 \
  | cut -f1 | nl | tr -d " " | sort -k2,2) \
 <(cat 49142202.txt | sort -k1,1 -k2nr,2) \
| sort --stable -k2n,2 | cut -f1,3
< p>按照第二列排序后,传递给join的第一个输入是:

2   A
1   B
3   C
< p > join 的第二个输入按列1排序为:

A   5
A   4
B   6
B   2
C   3
C   1

join的输出为:

A   2   5
A   2   4
B   1   6
B   1   2
C   3   3
C   3   1

然后按第二列中的“nl”行号进行排序,然后使用“cut”保留原始输入的第1列和第3列。

我知道使用Python的pandas库中的groupby可以更轻松地完成这个任务,但是是否有更优雅的方法,同时仍然使用GNU Coreutils,例如sortjoincuttrnl?最好能避免使用内存效率低下的awk解决方案,但也请分享这些方案。谢谢!


1
顺便提一下,既然你在谈论性能:cat 49142202.txt | sort -k2nr,2 是非常非常非常糟糕的做法,不需要在第一个命令中使用 cat。在两个命令中都是如此!!!https://superuser.com/questions/192052/advantages-of-cating-file-and-piping-to-grep - Allan
还要注意,使用许多管道并运行大量进程是不免费的,您必须考虑进程之间所有切换成本!最后但并非最不重要的是,将所有“sort”命令进行管道传输会严重减缓长文件的执行速度!!! - Allan
@Allan 好观点!我同意! - tommy.carstensen
嘿,最终你找到了什么最好的解决方案? - Allan
@Allan 工作有些繁忙,我在下个周末之前没有时间进行评估。我很高兴接受你的答案。请告诉我你的偏好。 - tommy.carstensen
请慢慢评估,如果您没有时间,也可以直接接受它 :p - Allan
3个回答

2

如评论中所解释的那样,我的解决方案试图减少管道数、不必要的cat命令,特别是管道sort操作的数量,因为排序是一个复杂/耗时的操作

我得出了以下解决方案,其中f_grp_sort是输入文件:

for elem in $(sort -k2nr f_grp_sort | awk '!seen[$1]++{print $1}')
do 
   grep $elem <(sort -k2nr f_grp_sort) 
done

输出:

B       6
B       2
A       5
A       4
C       3
C       1

说明:

sort -k2nr f_grp_sort 命令将生成以下输出:

B       6
A       5
A       4
C       3
B       2
C       1

sort -k2nr f_grp_sort | awk '!seen[$1]++{print $1}'将生成以下输出:

B
A
C

awk会按照同样的顺序生成临时输出的第一列中唯一的1个元素。

然后,for elem in $(...)do grep $elem <(sort -k2nr f_grp_sort); done将会搜索包含BAC的行,从而提供所需的输出结果。

现在,作为改进,您可以使用一个临时文件来避免重复执行sort -k2nr f_grp_sort操作:

$ sort -k2nr f_grp_sort > tmp_sorted_file && for elem in $(awk '!seen[$1]++{print $1}' tmp_sorted_file); do grep $elem tmp_sorted_file; done && rm tmp_sorted_file

1
所以,这种方法并不适用于所有情况,但如果您的第一列中的值可以转换为Bash变量,则我们可以使用动态命名数组来代替大量的联接。它应该很快。
第一个 while 块读取文件的内容,获取前两个用空格分隔的字符串,然后将它们放入 $col1 和 $col2 中。然后我们创建了一系列名为 `ARR_A` 和 `ARR_B` 的数组,其中 `A` 和 `B` 是第一列中的值(但仅当 `$col1` 只包含可以用作 Bash 变量名称的字符时)。该数组包含与这些列 1 值相关联的列 2 值。
我使用了您的复杂排序链来获得我们想要按列 1 值打印的顺序。我们只需循环遍历它们,然后对于每个列 1 数组,我们对值进行排序并回显列 1 和列 2。
动态变量部分可能难以理解,但对于列 1 中的正确值,它将起作用。同样,如果列 1 中有任何不能成为 Bash 变量名称的字符,则此解决方案将无法工作。
file=./49142202.txt

while read col1 col2 extra
do
  if [[ "$col1" =~ ^[a-zA-Z0-9_]+$ ]]
  then
    eval 'ARR_'${col1}'+=("'${col2}'")'
  else
    echo "Bad character detected in Column 1:  '$col1'"
    exit 1
  fi
done < "$file"

sort -k2nr,2 "$file" | sort --stable -k1,1 -u | sort -k2nr,2 | while read col1 extra
do 
  for col2 in $(eval 'printf "%s\n" "${ARR_'${col1}'[@]}"' | sort -r)
  do
    echo $col1 $col2
  done
done 

这是我的测试,比你提供的示例复杂一些:
$ cat 49142202.txt
A 4
B 6
C 3
A 5
B 2
C 1
C 0

$ ./run
B 6
B 2
A 5
A 4
C 3
C 1
C 0

1
非常感谢 @JeffBreadner 和 @Allan!我想出了另一种解决方案,与我的第一个解决方案非常相似,但提供了更多控制,因为它允许更容易地嵌套 for 循环。
for x in $(sort -k2nr,2 $file | sort --stable -k1,1 -u | sort -k2nr,2 | cut -f1); do
 awk -v x=$x '$1==x' $file | sort -k2nr,2
done

如果我需要时间来评估你们的解决方案的时间和内存性能,你们不介意我不接受你们的回答吧?否则我可能会选择@Allan的awk解决方案。


1
在进行一些测试时,我的解决方案将消除重复项,这可能不是您想要的(与其他解决方案相比,它相当复杂)。 tommy.carstensen和Allan提供的解决方案运行时间几乎相同。如果您编写的代码只有您自己会看到或维护,则始终更易于支持,但如果此代码将由其他人使用,则建议使用Allan的解决方案,因为在我看来它更加优雅。感谢tommy.carstensen提供了一个写得很好且有趣的问题:D - Jeff Breadner

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