複数のファイルの行を合計するためのAwkベースのソリューション

複数のファイルの行を合計するためのAwkベースのソリューション

次のファイルがいくつかあります。

ファイル1.dat:

1 1
1 3 4
5 9 10 11

ファイル2.dat:

3 0
8 9 0
3 9 2 4

通常、より多くの行があります(各行には、前の行よりも少ない列が含まれます)。上記の例を使用して、各ファイルの行を合計するハイブリッドbash / awkスクリプトを設計しました。

出力データ:

4 1
9 12 4
8 18 12 15

スクリプトは期待どおりに動作しますが、かなり遅いです。私のコンピュータでは、それぞれ10,000行の100個のファイルを処理するのに30分以上かかります。スクリプトは、すべてのファイルからn行目を収集するのにほとんどの時間を費やしているようです。file*.datawkコマンドに渡して私が行うことをする方法はありますか(下記参照)?

#!/bin/bash
ROWS=$1; shift
OUT_FILE=$1; shift
IN_FILE=("$@")

for i in `seq 1 1 ${ROWS}`; do
    # Get ith row from all input files
    for j in "${IN_FILE[@]}"; do
        tail -n+${i} ${j} | head -1 >> "temp.dat"
    done
    # Sum the rows 
    awk '{for (j=1;j<=NF;j++) a[j]+=$j} END {for (j in a) printf a[j] " "}' temp.dat >> ${OUT_FILE}
    echo >> ${OUT_FILE}
    rm temp.dat
done

上記の例に基づくスクリプトの使用法は次のとおりです。./RowSums.sh 3 out.dat file*.dat

答え1

anypasteとanyを使用してくださいawk

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

paste "${@}" |
awk -v numFiles="$#" '{
    numFldsPerFile = NF / numFiles
    for ( outFldNr=1; outFldNr<=numFldsPerFile; outFldNr++ ) {
        sum = 0
        for ( fileNr=1; fileNr<=numFiles; fileNr++ ) {
            inFldNr = outFldNr + (fileNr - 1) * numFldsPerFile
            sum += $inFldNr
        }
        printf "%d%s", sum, (outFldNr<numFldsPerFile ? OFS : ORS)
    }
}'

$ ./tst.sh file1.dat file2.dat
4 1
9 12 4
8 18 12 15

説明的な変数名と明示的なinFldNr計算によって、それが行うことを明確にすることができることを願っています。

答え2

次のawkスクリプトは、ほぼ完全なシェルスクリプトを置き換えることができます。

# cat rowsum.awk
FNR <= rows {
    for (i = 1; i <= NF; i++)
        sum[FNR,i] += $i
}
END {
    for (i = 1; i <= rows; i++) {
        for (j = 1; j <= rows + 1; j++) {
            printf "%s ", sum[i, j]
        }
        printf "\n";
    }
}

例:

% awk -f rowsum.awk -v rows=2 file*.dat
4 1
9 12 4
% awk -f rowsum.awk -v rows=3 file*.dat
4 1
9 12 4
8 18 12 15

これは、各行に対してすべてのファイルを繰り返しチェックするよりも高速です。

注: 私は仮定するN行にn+1リスト。それ以外の場合は、行ごとの列数(たとえばcols[FNR]=NF)を保存し、最終ループで使用します。


メモリ効率の高い別のオプションは、paste各ファイルから関連する行をすべてインポートすることです。

% paste -d '\n' file*.dat                                                                                                                                                
1 1
3 0
1 3 4
8 9 0
5 9 10 11
3 9 2 4

次に、次awkを使用します。

# cat rowsum-paste.awk
NR > 1 && NF != prevNF {
    for (i = 1; i <= prevNF; i++) {
        printf "%s ", sum[i];
        sum[i] = 0
    };
    printf "\n"
}
{
    for (i = 1; i <= NF; i++)
        sum[i] += $i;
    prevNF = NF
}
% (paste -d '\n' file*.dat; echo) | awk -f rowsum-paste.awk
4 1 
9 12 4 
8 18 12 15 

このawkコードは、フィールド数が変更されるまで行を合計し、現在の合計を印刷してリセットします。アドインは、echo最後のフィールド数を変更して最終印刷を開始することです。これはEND、ブロックの印刷コードをコピーすることによって行うこともできます。

答え3

awk最初の2つのフィールドの行インデックスと列インデックスを含み、その場所のデータ値を3番目のフィールドとして含むすべてのファイルのタブ区切りデータセットを出力するために使用されます。

awk -v OFS='\t' '{ for (i = 1; i <= NF; ++i) print FNR, i, $i }' file*.dat

データをソートし、datamashGNUを使用して上記で作成されたデータに対して操作を実行し、同じ(行、列)インデックスで発生する要素を合計します(このオプションは、欠落しているフィールドを置き換えるために何も出力しません)。crosstab--filler ''datamashN/A

sort -n | datamash --filler '' crosstab 1,2 sum 3

各列に追加されたヘッダーとdatamash出力行番号を含む最初の列を切り取ります。

tail -n +2 | cut -f 2-

問題の2つのファイルを考慮すると、これらすべてが出力とともに表示されます。

$ awk -v OFS='\t' '{ for (i = 1; i <= NF; ++i) print FNR, i, $i }' file*.dat | sort -n | datamash --filler '' crosstab 1,2 sum 3 | tail -n +2 | cut -f 2-
4       1
9       12      4
8       18      12      15

これをベンチマークして比較してみてください。ムルのソリューション、2つの小さなデータファイルでは、速度は4倍遅くなりません(3.7)。

関連情報