【问题标题】:Converting data in rows to columns将行中的数据转换为列
【发布时间】:2021-12-04 23:29:10
【问题描述】:

我的输入制表符分隔文件是这样的:

13435    830169  830264  a    95   y    16
09433    835620  835672  x    46
30945    838405  838620  a    21   c    19
94853    850475  850660  y    15
04958    865700  865978  c    16   a    98

在前三列之后,文件在下一列中显示变量及其值。我需要更改数据结构,以便在前三列之后,有这样的变量列:

                         a    x    y    c   
13435    830169  830264  95        16
09433    835620  835672       46
30945    838405  838620  21             19
94853    850475  850660            15
04958    865700  865978  98             16

在 linux 上是否有任何代码可以做到这一点? 文件大小为 7.6 MB,总行数约为 450,000。变量总数为四个。

谢谢

【问题讨论】:

  • bash pivot file 上尝试谷歌搜索;更有效的方法通常会使用awkperl 或编译语言;您的问题将有点独特,因为您只是旋转文件的一部分;您可能还想更新问题以包含更多详细信息,例如文件大小(# of MBytes,# of lines,# of unique letters - a/x/y/c/how_many_more?,输入/输出分隔符)
  • 不是最有效的方式(但可能更容易),但为什么不加载到某种关系数据库中并在那里做呢?
  • 你事先知道变量吗?
  • 非常感谢大家。所有答案都有效,真的帮助我学习。我想知道是否可以选择接受多个答案。
  • @dawg:是的,我事先知道变量。感谢您在下面的回答。

标签: bash pivot pivot-table


【解决方案1】:

假设:

  • 四个变量名(示例输入中的a/c/x/y)事先不知道
  • 变量后面总是有一个非空值
  • 事先不知道变量/值对的数量(在单个输入行上)
  • OP 可以按字母顺序打印变量列(OP 的期望输出未指定是否/如何对四个变量列进行排序)
  • 行的顺序保持不变(输入顺序 == 输出顺序)
  • 主机有足够的内存来保存整个输入文件在内存中(通过awk数组);这允许输入文件的单次传递;如果内存是一个问题(即输入文件无法放入内存),则需要不同的编码/设计(此答案中未解决)

另一个awk 想法...需要GNU awk 才能使用多维数组以及PROCINFO["sorted_in"] 构造:

awk '
BEGIN { FS=OFS="\t" }                             # input/output field delimiters = <tab>

      { first3[FNR]=$1 OFS $2 OFS $3              # store first 3 fields

        for (i=4;i<=NF;i=i+2) {                   # loop through rest of fields, 2 at a time
            vars[$i]                              # keep track of variable names
            values[FNR][$i]=$(i+1)                # store the value for this line/variable combo
        }
      }

END   { PROCINFO["sorted_in"]="@ind_str_asc"      # sort vars[] indexes in ascending order

        printf "%s%s", OFS, OFS                   # start printing header line ...
        for (v in vars)                           # loop through variable names ...
            printf "%s%s", OFS, v                 # printing to header line
        printf "\n"                               # terminate header line

        for (i=1;i<=FNR;i++) {                    # loop through our set of lines ...
            printf "%s",first3[i]                 # print the 1st 3 fields and then ...
            for (v in vars)                       # loop through list of all variables ...
                printf "%s%s",OFS,values[i][v]    # printing the associated value; non-existent values default to the empty string ""
            printf "\n"                           # terminate the current line of output
        }
      }
' inputfile

注意:这种设计允许处理可变数量的变量。

出于演示目的,我们将使用以下制表符分隔的输入文件:

$ cat input4                                         # OP's sample input file w/ 4 variables
13435   830169  830264  a       95      y       16
09433   835620  835672  x       46
30945   838405  838620  a       21      c       19
94853   850475  850660  y       15
04958   865700  865978  c       16      a       98

$ cat input6                                         # 2 additional variables added to OP's original input file
13435   830169  830264  a       95      y       16
09433   835620  835672  x       46      t       375
30945   838405  838620  a       21      c       19
94853   850475  850660  y       15      j       127     t       453
04958   865700  865978  c       16      a       98

通过awk 脚本运行这些生成:

############# input4
                        a       c       x       y
13435   830169  830264  95                      16
09433   835620  835672                  46
30945   838405  838620  21      19
94853   850475  850660                          15
04958   865700  865978  98      16

############# input6
                        a       c       j       t       x       y
13435   830169  830264  95                                      16
09433   835620  835672                          375     46
30945   838405  838620  21      19
94853   850475  850660                  127     453             15
04958   865700  865978  98      16

【讨论】:

    【解决方案2】:

    在纯 bash 中(需要 bash 4.0 或更高版本):

    #!/bin/bash
    
    declare -A var
    
    printf '\t\t\ta\tx\ty\tc\n'
    while IFS=$'\t' read -ra fld; do
        var[a]=""  var[x]=""  var[y]=""  var[c]=""
        for ((i = 3; i < ${#fld[@]}; i += 2)); do
            var["${fld[i]}"]=${fld[i + 1]}
        done
        printf '%s\t' "${fld[@]:0:3}"
        printf '%s\t%s\t%s\t%s\n' "${var[a]}" "${var[x]}" "${var[y]}" "${var[c]}"
    done < file
    

    【讨论】:

    • fwiw ... 这个答案要记住的一个问题是较大文件的性能;对于 OP 的 450K 行文件,所有 awk/perl 答案在 1-2 秒内运行,而这个“纯 bash”答案需要 90 多秒(在我的系统上)
    • @markp-fuso 这并不奇怪。 bash 在执行速度上无法与 awk 竞争。
    【解决方案3】:

    如果您知道您有 4 个变量 axyc,并且文件被格式化为制表符分隔文件,并且您想要显示的确切格式,您可以简单地使用一种“蛮力”方法,您可以在其中检查字段 46 的变量名称,并输出字段 57 的值,格式如使用 @987654329 所示@。

    例如,知道变量名称后,您可以简单地输出标题行,然后按如下方式处理每条记录:

    awk -F"\t" '
      FNR==1 { 
        print "\t\t\t  a    x    y    c"
      }
      {
        printf "%-8s%8s%8s  ", $1, $2, $3
        
        if ($4=="a")
          printf "%-5s", $5
        else if ($6=="a")
          printf "%-5s", $7
        else
          printf "%-5s", " "
        
        if ($4=="x")
          printf "%-5s", $5
        else if ($6=="x")
          printf "%-5s", $7
        else
          printf "%-5s", " "
        
        if ($4=="y")
          printf "%-5s", $5
        else if ($6=="y")
          printf "%-5s", $7
        else
          printf "%-5s", " "
        
        if ($4=="c")
          printf "%-5s\n", $5
        else if ($6=="c")
          printf "%-5s\n", $7
        else
          print ""
      }
    ' tabfile
    

    使用/输出示例

    如果您在tabfile 中输入,您将拥有:

    $ awk -F"\t" '
    >   FNR==1 {
    >     print "\t\t\t  a    x    y    c"
    >   }
    >   {
    >     printf "%-8s%8s%8s  ", $1, $2, $3
    >
    >     if ($4=="a")
    >       printf "%-5s", $5
    >     else if ($6=="a")
    >       printf "%-5s", $7
    >     else
    >       printf "%-5s", " "
    >
    >     if ($4=="x")
    >       printf "%-5s", $5
    >     else if ($6=="x")
    >       printf "%-5s", $7
    >     else
    >       printf "%-5s", " "
    >
    >     if ($4=="y")
    >       printf "%-5s", $5
    >     else if ($6=="y")
    >       printf "%-5s", $7
    >     else
    >       printf "%-5s", " "
    >
    >     if ($4=="c")
    >       printf "%-5s\n", $5
    >     else if ($6=="c")
    >       printf "%-5s\n", $7
    >     else
    >       print ""
    >   }
    > ' tabfile
                              a    x    y    c
    13435     830169  830264  95        16
    09433     835620  835672       46
    30945     838405  838620  21             19
    94853     850475  850660            15
    04958     865700  865978  98             16
    

    提供所需的输出。这种一次性方法对于 450,000 行输入也非常有效。由于这对于命令行脚本来说有点长,您可以简单地将它放在 awk 脚本中并使用文件名调用它。如果您有任何问题,请告诉我。

    作为脚本文件

    用作脚本文件,只需将内容放入文件并使其可执行,例如

    #!/usr/bin/awk -f
    
    BEGIN { FS="\t" }
    FNR==1 { 
      print "\t\t\t  a    x    y    c"
    }
    {
      printf "%-8s%8s%8s  ", $1, $2, $3
      
      if ($4=="a")
        printf "%-5s", $5
      else if ($6=="a")
        printf "%-5s", $7
      else
        printf "%-5s", " "
      
      if ($4=="x")
        printf "%-5s", $5
      else if ($6=="x")
        printf "%-5s", $7
      else
        printf "%-5s", " "
      
      if ($4=="y")
        printf "%-5s", $5
      else if ($6=="y")
        printf "%-5s", $7
      else
        printf "%-5s", " "
      
      if ($4=="c")
        printf "%-5s\n", $5
      else if ($6=="c")
        printf "%-5s\n", $7
      else
        print ""
    }
    

    另存为awkscript你会chmod +x awkscript然后运行:

    $ ./awkscript tabfile
                              a    x    y    c
    13435     830169  830264  95        16
    09433     835620  835672       46
    30945     838405  838620  21             19
    94853     850475  850660            15
    04958     865700  865978  98             16
    

    【讨论】:

    • 抱歉,问题没有更清楚。一行可能有两个以上的变量。无论如何,感谢您提供非常详细的答案。
    【解决方案4】:
    input="\
    13435   830169  830264  a   95  y   16
    09433   835620  835672  x   46
    30945   838405  838620  a   21  c   19
    94853   850475  850660  y   15
    04958   865700  865978  c   16  a   98
    "
    

    awk:

    printf '\t\t\ta\tx\ty\tc\n'
    echo -n "$input" |
    awk -v vars='a x y c' '
      BEGIN {NV = split(vars,V)}
      {
         s = $1 "\t" $2 "\t" $3;
         delete a;
         for(i = 4; i < NF; i = i+2) a[$i] = $(i+1);
         for(i = 1; i <= NV; i++) s = s "\t" a[V[i]];
         print s
      }
    '
    

    用红宝石:

    printf '\t\t\ta\tx\ty\tc\n'
    echo -n "$input" |
    vars='a x y c' ruby -ane '
        BEGIN{v = ENV["vars"].split};
        h = Hash[*$F[3..-1]];
        puts $F[0..2].concat(v.map{|v| h[v]}).join("\t")
    '
    

    输出:

                a   x   y   c
    13435   830169  830264  95      16  
    09433   835620  835672      46      
    30945   838405  838620  21          19
    94853   850475  850660          15  
    04958   865700  865978  98          16
    

    【讨论】:

      【解决方案5】:

      perl:

      $ perl -lane '
          BEGIN { print join("\t", "", "", "", "a", "x", "y", "c"); }
          my %vars = @F[3..$#F];
          print join("\t", @F[0..2], @vars{qw/a x y c/});
        ' input.tsv
                              a       x       y       c
      13435   830169  830264  95              16
      09433   835620  835672          46
      30945   838405  838620  21                      19
      94853   850475  850660                  15
      04958   865700  865978  98                      16
      

      第四列和后面的所有列都作为哈希表的键/值对,然后按照正确的顺序提取其中的变量值,以及前三列。大量使用slices

      【讨论】:

        【解决方案6】:

        这里有一个 awk 可以做到这一点:

        awk '
        BEGIN{fmt="%s\t%s\t%s\t%s\t%s\t%s\t%s\n"}
        NR==FNR{if ($4 && !($4 in seen)) {
                    seen[$4]=++col; cols[col]=$4
                }
                if ($6 && !($6 in seen)) {
                    seen[$6]=++col; cols[col]=$6
                }
            next
        }
        FNR==1{printf fmt, "\t","\t","\t",cols[1],cols[2],cols[3],cols[4]}
        {   split("",fields)
            fields[seen[$4]]=$5; fields[seen[$6]]=$7
            printf fmt, $1,$2,$3,fields[1],fields[2],fields[3],fields[4]
        }
        ' file file 
        

        这将找到变量名称并按照第一次看到的顺序打印它们。

        打印:

                                a   y   x   c
        13435   830169  830264  95  16      
        09433   835620  835672          46  
        30945   838405  838620  21          19
        94853   850475  850660      15      
        04958   865700  865978  98          16
        

        如果你事先知道你的变量并想说明列的顺序,你可以这样做:

        awk '
        BEGIN{
            fmt="%s\t%s\t%s\t%s\t%s\t%s\t%s\n"
            seen["a"]=1;seen["x"]=2;seen["y"]=3;seen["c"]=4
        }
        
        FNR==1{printf fmt, "\t","\t","\t","a","x","y","c"}
        {   split("",fields)
            fields[seen[$4]]=$5; fields[seen[$6]]=$7
            printf fmt, $1,$2,$3,fields[1],fields[2],fields[3],fields[4]
        }
        ' file
        

        打印:

                                a   x   y   c
        13435   830169  830264  95      16  
        09433   835620  835672      46      
        30945   838405  838620  21          19
        94853   850475  850660          15  
        04958   865700  865978  98          16
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2022-09-24
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多