重复数据但行数不同的连续排序

Posted

技术标签:

【中文标题】重复数据但行数不同的连续排序【英文标题】:Successive sorting of repeating data but different number of lines 【发布时间】:2021-09-25 16:47:46 【问题描述】:

我有一个格式如下的数据:

2
1
A    11.364500   48.395199   40.160500         5
B    35.067699   30.944700   24.155300         4
4
2
A    26.274099   38.533298   32.624298         4
B    36.459000   29.662701   17.806299         5
A    15.850300   28.130899   24.753901         4
A    32.098000   33.665699   20.372499         4
5
3
A    17.501801   44.279202    8.005670         5
B    35.853001   43.095901   17.402901         4
B     1.326100   17.127600   39.600300         4
A     9.837760   41.103199   13.062300         5
B    31.686800   44.997501   16.619499         4
3
4
B    31.274700    8.726580   25.267599         4
A    19.032400   41.384701   19.456301         5
B    19.441900   24.286400    6.961680         4
1
5
B    48.973999   15.508400    5.099710         4
6
6
A    42.586601   21.343800   23.280701         5
B    30.145800   13.256200   30.713800         4
B    29.186001   44.353699    9.057160         4
A    39.311199   27.371201   35.473999         5
B    31.437799   30.415001   37.454399         4
B    48.501999    1.277730   21.900600         4
...

数据由多个重复的数据块组成。每个数据块的格式相同,如下:

First line = Number of lines of data block
Second line = ID of data block
From third line = Actual data lines (with number of lines designated in the first line)

例如,如果我们看到第一个数据块:

2 (=> This data block has 2 number of lines) 
1 (=> This data block's ID is 1 (very first data block)) 
A    11.364500   48.395199   40.160500         5
B    35.067699   30.944700   24.155300         4
(Third and fourth lines of the data block are actual data. 
 The integer of the first line, the number of lines for this data block, is 2, so the actual data consist of 2 lines. 
 All other data blocks follow the same format. ) 
    前两行是每个重复数据块的标题。第一行指定数据块的行数,第二行是数据块的ID。 然后真正的数据从每个数据块的第三行开始。正如我所解释的,“真实数据”的行数在第一行中指定为整数。 因此,每个数据块的总行数将为 number_of_lines+2。在本例中,数据块 1 的总行数为 4,数据块 2 花费 6 行...

这种格式重复 100,000 次。换句话说,我在单个文本文件中有 100,000 个数据块,就像这个例子一样。我可以提供数据块的总数。

我要做的是按第四列对每个数据块的“实际数据行”进行排序,并以与原始数据文件相同的格式打印出数据。我希望从上面的示例中实现的效果如下:

2
1
A    11.364500   48.395199   40.160500         5
B    35.067699   30.944700   24.155300         4
4
2
A    26.274099   38.533298   32.624298         4
A    15.850300   28.130899   24.753901         4
A    32.098000   33.665699   20.372499         4
B    36.459000   29.662701   17.806299         5
5
3
B     1.326100   17.127600   39.600300         4
B    35.853001   43.095901   17.402901         4
B    31.686800   44.997501   16.619499         4
A     9.837760   41.103199   13.062300         5
A    17.501801   44.279202    8.005670         5
3
4
B    31.274700    8.726580   25.267599         4
A    19.032400   41.384701   19.456301         5
B    19.441900   24.286400    6.961680         4
1
5
B    48.973999   15.508400    5.099710         4
6
6
B    31.437799   30.415001   37.454399         4
A    39.311199   27.371201   35.473999         5
B    30.145800   13.256200   30.713800         4
A    42.586601   21.343800   23.280701         5
B    48.501999    1.277730   21.900600         4
B    29.186001   44.353699    9.057160         4
...

我知道在 Linux 中使用第 4 列对数据进行排序的典型命令是:

sort -gk4 data.txt > data_4sort.txt

但是,在这种情况下,我需要对每个数据块进行排序。另外,每个数据块的行数并不统一,但每个数据块的数据行数不同。

我真的不知道我该如何解决这个问题。我正在考虑使用带有 for 循环和/或 awk、sed、split 等的 shell 脚本和 sort 命令。但是我什至不确定如何使用 for 循环来解决这个问题,或者是否真的需要 for 循环和 shell 脚本来执行这种逐块排序。

【问题讨论】:

【参考方案1】:

这可以使用awksort 的组合来完成。你使用awk 来驱动整个过程,就像一个非常简单的状态机,它遵循以下规则:

有两种状态,表示您正在处理的线型组。 线条类型有短(状态 0,一个字段)和长(状态 1,更多字段)。 根据当前状态和正在处理的当前行,会发生不同的操作。 需要排序的项目将临时写入排序文件,并在需要时进行排序。

动作基本上是针对每一行的。

简而言之: 对于短行,只需打印即可。 否则(长行),仅使用该行创建新的排序文件,将状态设置为长。 否则(处于长状态): 如果行长,只需将其附加到排序文件即可。 否则(短行),排序文件,打印短行,将状态设置为短行。

最后,如果需要,对排序文件进行排序/打印/删除(因为在最后一个块之后不会有从长行到短行的最终转换,所以这个操作会解决这个问题)。

该处理按以下方式实现,一个调用awk 来完成工作的shell 脚本:

awk '
    function sort() 
        close(tmp_file)
        system("sort -rnk4 "tmp_file"; rm -rf "tmp_file)
    
    BEGIN  tmp_file = "/tmp/tmp_file" 
    state == 0 && NF == 1  print ; next 
    state == 0 && NF != 1  print >tmp_file ; state = 1 ; next 
    state == 1 && NF != 1  print >>tmp_file ; next 
    state == 1 && NF == 1  sort() ; print ; state = 0 ; next 
    END  if (state == 1)  sort()  
' input_file

您也可以使用足够高级的awk 变体的临时文件来执行此操作,该变体允许管道:

awk 'NF > 1  print | "sort -rnk4"; next 
             close("sort -rnk4"); print 
' input_file

这样做的好处是一个更简单、更短的awk 程序,它隐式地完成了前面脚本中的大部分内容。

在您的输入文件上运行任一个这些脚本会根据要求生成按第四列(最终浮点数)排序的每个单独分组:

2
1
A    11.364500   48.395199   40.160500         5
B    35.067699   30.944700   24.155300         4
4
2
A    26.274099   38.533298   32.624298         4
A    15.850300   28.130899   24.753901         4
A    32.098000   33.665699   20.372499         4
B    36.459000   29.662701   17.806299         5
5
3
B     1.326100   17.127600   39.600300         4
B    35.853001   43.095901   17.402901         4
B    31.686800   44.997501   16.619499         4
A     9.837760   41.103199   13.062300         5
A    17.501801   44.279202    8.005670         5
3
4
B    31.274700    8.726580   25.267599         4
A    19.032400   41.384701   19.456301         5
B    19.441900   24.286400    6.961680         4
1
5
B    48.973999   15.508400    5.099710         4
6
6
B    31.437799   30.415001   37.454399         4
A    39.311199   27.371201   35.473999         5
B    30.145800   13.256200   30.713800         4
A    42.586601   21.343800   23.280701         5
B    48.501999    1.277730   21.900600         4
B    29.186001   44.353699    9.057160         4

【讨论】:

创建显式临时文件有好处吗? IE。为什么不只在 awk 中使用 print | "sort ..."?是性能问题还是可移植性问题? 谢谢。我还有一个要求。事实上,“真实数据”的第一列可以有 2 个字母字符,如“AA”或“AB”。抱歉,我忘记在示例中提及这一点。这种情况下需要在awk/sort命令中修改什么? @exsonic01: 什么都不需要改变,“长”行完全按原样写入文件,排序只是对字段 4 进行反向数字排序。 关于 an advanced enough awk variant - 在任何 awk 中的行为方式都相同。 这会很慢,因为您要生成一个子shell 来为每个输入块调用一次“排序”。你在做shell awk FOR each line DO system shell sort ENDFOR 。在 awk 中不生成子壳会更有效,而只需执行 shell awk | sort 或类似操作。如果在脚本开始之前就存在 /tmp/tmp_file ,它也会破坏 - 最好调用 mktemp 来创建任何临时文件。【参考方案2】:

一个选项可以在不显式创建临时文件的情况下通过管道传递到以下命令。

awk '
BEGIN 
  cmd = "sort -k4,4nr"


  if(NF > 1)
    print | cmd
  
  else
    close(cmd)
    print
  
' input_file

注意:由于@Ed Morton 的有用提示,已更改为数字排序并将 cmd 移动到变量

输出:

2
1
A    11.364500   48.395199   40.160500         5
B    35.067699   30.944700   24.155300         4
4
2
A    26.274099   38.533298   32.624298         4
A    15.850300   28.130899   24.753901         4
A    32.098000   33.665699   20.372499         4
B    36.459000   29.662701   17.806299         5
5
3
B     1.326100   17.127600   39.600300         4
B    35.853001   43.095901   17.402901         4
B    31.686800   44.997501   16.619499         4
A     9.837760   41.103199   13.062300         5
A    17.501801   44.279202    8.005670         5
3
4
B    31.274700    8.726580   25.267599         4
A    19.032400   41.384701   19.456301         5
B    19.441900   24.286400    6.961680         4
1
5
B    48.973999   15.508400    5.099710         4
6
6
B    31.437799   30.415001   37.454399         4
A    39.311199   27.371201   35.473999         5
B    30.145800   13.256200   30.713800         4
A    42.586601   21.343800   23.280701         5
B    48.501999    1.277730   21.900600         4
B    29.186001   44.353699    9.057160         4

【讨论】:

不错的答案,唯一的缺点是它会很慢,因为 awk 必须分拆一个子外壳来调用 sort 每个输入记录一次。仅供参考,在任何 awk 中的行为方式都相同。我建议将其设为BEGIN cmd="sort -k4,4r" and then print | cmd` 和 close(cmd) 以避免将来发生任何更改,即您在一个地方更改命令而忘记在另一个地方更改它并最终出现神秘错误。 @EdMorton 谢谢你的解释。您能否指出有关其工作原理的文档?直觉上很难理解它如何在每一行上调用一个新的排序,因为排序本质上应该一次需要处理多个行才能工作。但是,这对我来说可能不直观,因为我是 unix 工具的新手 :) 它不会对每一行调用一个新的排序,而是对每条记录调用一个新的排序(即具有 > 1 个字段的行块)。在您的代码中显示print | "sort -k4,4r",如果没有这样的调用“sort”的子shell正在运行,那么awk会生成一个新的子shell来调用“sort”,但是如果这样的子shell已经在运行,那么它只是将当前行通过管道传递给它.现在看看你的elseclose("sort -k4,4r") 正是这样做的,它关闭管道以“排序”从而杀死子shell。所以下次if (NF > 1) 变为真时,awk 必须重新生成一个子shell。 在gnu.org/software/gawk/manual/gawk.html#I_002fO-Functions上有更多信息。 如果不是 100% 清楚,请随时提问。【参考方案3】:

使用 awk 准备文本以进行排序,然后将其通过管道传递给 1 次对 sort 的调用,而不会产生子 shell,也不会创建临时文件:

$ cat tst.sh
#!/usr/bin/env bash

awk '
BEGIN  OFS="\t"; recNr=1 
NF < pNF  ++recNr 

    print recNr, NF, NR, $0
    pNF = NF

' "$@:--" |
sort -k1,1n -k2,2n -k7,7nr -k3,3n |
cut -f4-

$ ./tst.sh file
2
1
A    11.364500   48.395199   40.160500         5
B    35.067699   30.944700   24.155300         4
4
2
A    26.274099   38.533298   32.624298         4
A    15.850300   28.130899   24.753901         4
A    32.098000   33.665699   20.372499         4
B    36.459000   29.662701   17.806299         5
5
3
B     1.326100   17.127600   39.600300         4
B    35.853001   43.095901   17.402901         4
B    31.686800   44.997501   16.619499         4
A     9.837760   41.103199   13.062300         5
A    17.501801   44.279202    8.005670         5
3
4
B    31.274700    8.726580   25.267599         4
A    19.032400   41.384701   19.456301         5
B    19.441900   24.286400    6.961680         4
1
5
B    48.973999   15.508400    5.099710         4
6
6
B    31.437799   30.415001   37.454399         4
A    39.311199   27.371201   35.473999         5
B    30.145800   13.256200   30.713800         4
A    42.586601   21.343800   23.280701         5
B    48.501999    1.277730   21.900600         4
B    29.186001   44.353699    9.057160         4

这是上述 OPs 输入文件在执行速度上的第 3 次运行时间差异:

$ time ./tst.sh file >/dev/null

real    0m0.105s
user    0m0.030s
sys     0m0.091s

当前的 awk 答案会为每个要排序的输入块生成一个子shell:

@IceCreamTucan's answer:

$ cat tst2.sh
#!/usr/bin/env bash

awk '
  if(NF > 1)
    print | "sort -k4,4nr"
  
  else
    close("sort -k4,4nr")
    print
  
' "$@:--"

$ time ./tst2.sh file >/dev/null

real    0m0.405s
user    0m0.120s
sys     0m0.121s

@paxdiablo's answer:

$ cat tst3.sh
#!/usr/bin/env bash

awk '
    function sort() 
        close(tmp_file)
        system("sort -rnk4 "tmp_file"; rm -rf "tmp_file)
    
    BEGIN  tmp_file = "/tmp/tmp_file" 
    state == 0 && NF == 1  print ; next 
    state == 0 && NF != 1  print >tmp_file ; state = 1 ; next 
    state == 1 && NF != 1  print >>tmp_file ; next 
    state == 1 && NF == 1  sort() ; print ; state = 0 ; next 
    END  if (state == 1)  sort()  
' "$@:--"

$ time ./tst3.sh file >/dev/null

real    0m0.804s
user    0m0.060s
sys     0m0.396s

【讨论】:

我会给你一个投票,Ed,它比我的性能更好(考虑到 OP 规定了 100,000 个块,这可能很重要)。干杯。【参考方案4】:

我知道,老问题,但是...另一个想法,使用更新的gawk 能力更高效...


使用gawk 及其predefined array scanning orders 的一个想法(即,无需外部sort)...

awk '
BEGIN  delete arr                             # hack to pre-declare variable arr as an array

function sort_block() 

    if ( length(arr) > 0 )                     # if array is not empty then ...
       PROCINFO["sorted_in"]="@ind_num_desc"    # desginate sorting by index/descending and then ...
       for (i in arr)                           # loop through array ...
           print arr[i]                         # printing arry entries
    
    delete arr                                  # clear array


NF==1  sort_block()
        print
      
NF>1   arr[$4]=$0
        next
      
END    sort_block()                           # flush anything still in array
' block.dat

针对 OPs 样本数据集运行此操作会生成:

2
1
A    11.364500   48.395199   40.160500         5
B    35.067699   30.944700   24.155300         4
4
2
A    26.274099   38.533298   32.624298         4
A    15.850300   28.130899   24.753901         4
A    32.098000   33.665699   20.372499         4
B    36.459000   29.662701   17.806299         5
5
3
B     1.326100   17.127600   39.600300         4
B    35.853001   43.095901   17.402901         4
B    31.686800   44.997501   16.619499         4
A     9.837760   41.103199   13.062300         5
A    17.501801   44.279202    8.005670         5
3
4
B    31.274700    8.726580   25.267599         4
A    19.032400   41.384701   19.456301         5
B    19.441900   24.286400    6.961680         4
1
5
B    48.973999   15.508400    5.099710         4
6
6
B    31.437799   30.415001   37.454399         4
A    39.311199   27.371201   35.473999         5
B    30.145800   13.256200   30.713800         4
A    42.586601   21.343800   23.280701         5
B    48.501999    1.277730   21.900600         4
B    29.186001   44.353699    9.057160         4

在笔记本电脑级 i7(即,这绝不是快速系统)上的 VM 内的 cygwin 环境中重复运行此代码可提供以下最佳时机:

real    0m0.026s
user    0m0.000s
sys     0m0.015s

【讨论】:

以上是关于重复数据但行数不同的连续排序的主要内容,如果未能解决你的问题,请参考以下文章

使用 predict() 时,预测变量数相同但行数不同的新数据出错

如何查找和计算两个不同数据帧之间的重复行数? [关闭]

插入大查询表的行数少于预期

为列中的每个唯一值分配值[重复]

使用规则在 R 中使用重复项目进行购物篮分析

如何使用不同的行数更新 QAbstractTableModel 中的数据