【问题标题】:How to subtract values of a specific row value from all the other row values?如何从所有其他行值中减去特定行值的值?
【发布时间】:2019-07-10 14:38:31
【问题描述】:

我现在的工作文件是这样的

ID   Time   A_in   Time  B_in  Time  C_in
Ax   0.1    10     0.1   15    0.1   45  
By   0.2    12     0.2   35    0.2   30  
Cz   0.3    20     0.3   20    0.3   15  
Fr   0.4    35     0.4   15    0.4   05  
Exp  0.5    10     0.5   25    0.5   10

我感兴趣的列是带有"_in" 标题的列。在这些列中,我想从 ID 为“Exp”的行元素中减去所有行元素的值。 让我们考虑A_in 列,其中"Exp" 行值为10。所以我想从A_in 列的所有其他元素中减去10

我的业余代码是这样的(我知道很傻)

#This part is grabbing all the values in ```Exp``` row
Exp=$( awk 'BEGIN{OFS="\t";
            PROCINFO["sorted_in"] = "@val_num_asc"}
    FNR==1 { for (n=2;n<=NF;n++) { if ($n ~ /_GasOut$/) cols[$n]=n; }}
    /Exp/ {
           for (c in cols){
           shift = $cols[c]
           printf shift" "
           }
       }

        ' File.txt |paste -sd " ") 
Exp_array=($Exp)

z=1
for i in "${Exp_array[@]}"
do
z=$(echo 2+$z | bc -l)
Exp_point=$i
awk  -vd="$Exp_point" -vloop="$z" -v  '
            BEGIN{OFS="\t";
            PROCINFO["sorted_in"] = "@val_num_asc"}
            function abs(x) {return x<0?-x:x}
            FNR==1 { for (n=2;n<=NF;n++) { if ($n ~ /_GasOut$/) cols[$n]=n; }}
        NR>2{
            $loop=abs($loop-d); print
            }
         ' File.txt
done

我第一个想要的结果是这样的

ID   Time   A_in   Time  B_in  Time  C_in
Ax   0.1    0.0    0.1   10    0.1   35  
By   0.2    02     0.2   10    0.2   20  
Cz   0.3    10     0.3   05    0.3   05  
Fr   0.4    25     0.4   10    0.4   05  
Exp  0.5    0.0    0.5   0.0   0.5  0.0

现在,我想从每个 "_in" 列中找到 2 个最小值的对应 ID。所以 我的第二个期望结果是

A_in   B_in  C_in
Ax     Cz    Cz 
By     Exp   Fr 
Exp          Exp

【问题讨论】:

  • 为什么 B_in:Ax = 10 而不是 -10?
  • 我采用绝对值。
  • 为什么 02 而不仅仅是 2A_in,By 中?还有05?这个格式是printf %2.0f 还是printf %2.1f
  • 没有,我只是准备了一些随机数据。没有格式。

标签: bash awk


【解决方案1】:

Perl 来救援!

#!/usr/bin/perl
use warnings;
use strict;
use feature qw{ say };

@ARGV = (@ARGV[0, 0]);  # Read the input file twice.

my @header = split ' ', <>;
my @in = grep $header[$_] =~ /_in$/, 0 .. $#header;
$_ = <> until eof;
my @exp = split;

my @min;
<>;
while (<>) {
    my @F = split;
    for my $i (@in) {
        $F[$i] = abs($F[$i] - $exp[$i]);
        @{ $min[$i] }[0, 1]
            = sort { $a->[0] <=> $b->[0] }
                   [$F[$i], $F[0]], grep defined, @{ $min[$i] // [] }
            unless eof;
    }
    say join "\t", @F;
}

print "\n";
say join "\t", @header[@in];
for my $index (0, 1) {
    for my $i (@in) {
        next unless $header[$i] =~ /_in$/;
        print $min[$i][$index][1], "\t";
    }
    print "\n";
}

它读取文件两次。在第一次读取时,它只记住第一行为@header 数组,最后一行为@exp 数组。

在第二次读取中,它从每个 _in 列中减去相应的 exp 值。它还将@min数组中与列位置对应的位置存储了两个最小的数字。

格式化数字(即0.0而不是002而不是2)作为练习留给读者。与将输出重定向到几个不同的文件相同。

【讨论】:

  • 谢谢,但我怎样才能将它包含在 bash 脚本中?我不知道 perl。
  • 将其保存到名为xyz 的文件中,使用perl xyz inputfile 调用它。
  • 你可以通过运行awk 'script' filename filenameawk中读取文件两次
  • @Barmar:与 Perl 相同。但是,如果脚本应该总是读取同一个文件两次,我发现将逻辑包含到脚本中更合适,而不是调用它。
  • 只有有人写了才会有。
【解决方案2】:

在玩了一两个小时后,我写了这篇可憎的文章:

cat <<EOF >file
ID   Time   A_in   Time  B_in  Time  C_in
Ax   0.1    10     0.1   15    0.1   45  
By   0.2    12     0.2   35    0.2   30  
Cz   0.3    20     0.3   20    0.3   15  
Fr   0.4    35     0.4   15    0.4   05  
Exp  0.5    10     0.5   25    0.5   10
EOF
# fix stackoverflow formatting
# input file should be separated with tabs
<file tr -s ' ' | tr ' ' '\t' > file2
mv file2 inputfile

# read headers to an array
IFS=$'\t' read -r -a hdrs < <(head -n1 inputfile)

# exp line read into an array
IFS=$'\t' read -r -a exps < <(grep -m1 $'^Exp\t' inputfile)

# column count
colcnt="${#hdrs[@]}"
if [ "$colcnt" -eq 0 ]; then 
    echo >&2 "ERROR - must be at least one column"
    exit 1
fi

# numbers of those columns which headers have _in suffix
incolnums=$(
    paste <(
        printf "%s\n" "${hdrs[@]}"
    ) <(
        # puff, the numbers will start from zero cause bash indexes arrays from zero
        # but `cut` indexes fields from 1, so.. just keep in mind it's from 0
        seq 0 $((colcnt - 1))
    ) |
    grep $'_in\t' |
    cut -f2
)

# read the input file
{
    # preserve header line
    IFS= read -r hdrline
    ( IFS=$'\t'; printf "%s\n" "$hdrline" )

    # ok. read the file field by field
    # I think we could awk here
    while IFS=$'\t' read -a vals; do

        # for each column number with _in suffix
        while IFS= read -r incolnum; do

            # update the column value
            # I use bc for float calculations
            vals[$incolnum]=$(bc <<-EOF
                define abs(i) {
                    if (i < 0) return (-i)
                    return (i)
                }
                scale=2
                abs(${vals[$incolnum]} - ${exps[$incolnum]})
EOF
            )

        done <<<"$incolnums"

        # output the line
        ( IFS=$'\t'; printf "%s\n" "${vals[*]}" )

    done

} < inputfile > MyFirstDesiredOutcomeIsThis.txt

# ok so, first part done

{
    # output headers names with _in suffix
    printf "%s\n" "${hdrs[@]}" | 
    grep '_in$' |
    tr '\n' '\t' |
    # omg, fix tr, so stupid
    sed 's/\t$/\n/'

    # puff
    # output the corresponding ID of 2 smallest values of the specified column number
    # @arg: $1 column number
    tmpf() {
        # remove header line
        <MyFirstDesiredOutcomeIsThis.txt tail -n+2 |
        # extract only this column
        cut -f$(($1 + 1)) |
        # unique numeric sort and extract two smallest values
        sort -n -u | head -n2 |
        # now, well, extract the id's that match the numbers
        # append numbers with tab (to match the separator)
        # suffix numbers with dollar (to match end of line)
        sed 's/^/\t/; s/$/$/;' |
        # how good is grep at buffering(!)
        grep -f /dev/stdin <(
            <MyFirstDesiredOutcomeIsThis.txt tail -n+2 |
            cut -f1,$(($1 + 1))
        ) |
        # extract numbers only
        cut -f1
    }

    # the following is something like foldr $'\t' $(tmpf ...) for each $incolnums
    # we need to buffer here, we are joining the output column-wise
    output=""
    while IFS= read -r incolnum; do
        output=$(<<<$output paste - <(tmpf "$incolnum"))
    done <<<"$incolnums"

    # because with start with empty $output, paste inserts leading tabs
    # remove them ... and finally output $output
    <<<"$output" cut -f2-

}  > MySecondDesiredOutcomeIs.txt

# fix formatting to post it on stackoverflow
# files have tabs, and column will output them with space
# which is just enough
echo '==> MyFirstDesiredOutcomeIsThis.txt <=='
column -t -s$'\t' MyFirstDesiredOutcomeIsThis.txt
echo
echo '==> MySecondDesiredOutcomeIs.txt <=='
column -t -s$'\t' MySecondDesiredOutcomeIs.txt

脚本会输出:

==> MyFirstDesiredOutcomeIsThis.txt <==
ID   Time  A_in  Time  B_in  Time  C_in
Ax   0.1   0     0.1   10    0.1   35
By   0.2   2     0.2   10    0.2   20
Cz   0.3   10    0.3   5     0.3   5
Fr   0.4   25    0.4   10    0.4   5
Exp  0.5   0     0.5   0     0.5   0

==> MySecondDesiredOutcomeIs.txt <==
A_in  B_in  C_in
Ax    Cz    Cz
By    Exp   Fr
Exp         Exp

tutorialspoint 编写和测试。

我使用 bash 和 core-/more-utils 来操作文件。首先,我确定以_in 后缀结尾的列数。然后我缓冲存储在Exp 行中的值。

然后我只是逐行、逐个字段地读取文件,并且对于每个具有标题以_in 后缀结尾的列号的字段,我用@987654327 中的字段值减去字段值@ 线。我认为这部分应该是最慢的(我使用普通的while IFS=$'\t' read -r -a vals),但是一个聪明的awk 脚本可以加快这个过程。正如您所说,这会生成您的“第一个所需输出”。

然后我只需要输出以_in 后缀结尾的标题名称。然后对于以_in 后缀结尾的每个列号,我需要确定列中的 2 个最小值。我使用普通的sort -n -u | head -n2。然后,它变得有点棘手。我需要提取在此类列中具有相应 2 个最小值之一的 ID。这是grep -f 的工作。我使用sed 在输入中准备适当的正则表达式,并让grep -f /dev/stdin 完成过滤工作。

【讨论】:

    【解决方案3】:

    请一次只问 1 个问题。以下是您询问的第一件事的方法:

    $ cat tst.awk
    BEGIN   { OFS="\t" }
    NR==FNR { if ($1=="Exp") split($0,exps); next }
    FNR==1  { $1=$1; print; next }
    {
        for (i=1; i<=NF; i++) {
            val = ( (i-1) % 2 ? $i : exps[i] - $i )
            printf "%s%s", (val < 0 ? -val : val), (i<NF ? OFS : ORS)
        }
    }
    
    $ awk -f tst.awk file file
    ID      Time    A_in    Time    B_in    Time    C_in
    0       0.1     0       0.1     10      0.1     35
    0       0.2     2       0.2     10      0.2     20
    0       0.3     10      0.3     5       0.3     5
    0       0.4     25      0.4     10      0.4     5
    0       0.5     0       0.5     0       0.5     0
    

    在每个 UNIX 机器上的任何 shell 中使用任何 awk 都将有效且稳健地工作。

    如果在阅读本文后,重新阅读您收到的之前的 awk 答案,并查找 awk 手册页,您仍然需要关于您询问的第二件事的帮助,然后询问新的独立就是这个问题。

    【讨论】:

      猜你喜欢
      • 2021-10-03
      • 2018-10-07
      • 1970-01-01
      • 2013-10-31
      • 2020-09-24
      • 2015-07-27
      • 2015-09-04
      • 1970-01-01
      • 2019-09-09
      相关资源
      最近更新 更多