awkの魅力を伝える: 「合計・平均・階差などを取得」

2022/03/15に公開

はじめに

以下の記事の続きとなっています。
https://zenn.dev/nutmeg/articles/awk_select_10

今回はawkを用いた「集計」をテーマにしています。以下のテクニックを扱います。

使用するテストデータ

以下のデータを使用します。

  • openweatherdata.csv
  • transport-inputdata-22-1.csv

1つ目は以前の記事に貼っているものと同じになります。念のためこちらにも掲載します。
今回は集計のために疑似の交通費申請フォームを追加して、使用します。

クリックして表示
openweatherdata.csv
日付,時刻,天気,説明,気温,最高気温,最低気温,湿度
2022-03-05,03:00:00,Clouds,曇りがち,8.21,9.04,8.21,75
2022-03-05,06:00:00,Clouds,,8.09,8.24,8.09,72
2022-03-05,09:00:00,Clouds,,9.51,9.51,9.51,57
2022-03-05,12:00:00,Clear,晴天,13.28,13.28,13.28,32
2022-03-05,15:00:00,Clear,晴天,17.62,17.62,17.62,27
2022-03-05,18:00:00,Clear,晴天,15.99,15.99,15.99,30
2022-03-05,21:00:00,Clear,晴天,13.77,13.77,13.77,35
2022-03-06,00:00:00,Clear,晴天,9.61,9.61,9.61,35
2022-03-06,03:00:00,Clouds,薄い雲,7.41,7.41,7.41,32
2022-03-06,06:00:00,Clouds,曇りがち,7.31,7.31,7.31,27
2022-03-06,09:00:00,Clouds,曇りがち,9.04,9.04,9.04,26
2022-03-06,12:00:00,Clouds,曇りがち,10.71,10.71,10.71,21
2022-03-06,15:00:00,Clouds,曇りがち,10.88,10.88,10.88,20
2022-03-06,18:00:00,Clouds,,8.24,8.24,8.24,26
2022-03-06,21:00:00,Clouds,薄い雲,7.25,7.25,7.25,29
2022-03-07,00:00:00,Clear,晴天,6.7,6.7,6.7,30
2022-03-07,03:00:00,Clear,晴天,5.89,5.89,5.89,33
2022-03-07,06:00:00,Clear,晴天,5.49,5.49,5.49,36
2022-03-07,09:00:00,Clouds,,7.92,7.92,7.92,24
2022-03-07,12:00:00,Clouds,厚い雲,10.48,10.48,10.48,24
2022-03-07,15:00:00,Clouds,厚い雲,11.74,11.74,11.74,38
2022-03-07,18:00:00,Clouds,厚い雲,9.66,9.66,9.66,59
2022-03-07,21:00:00,Rain,小雨,8.2,8.2,8.2,60
2022-03-08,00:00:00,Rain,小雨,7.56,7.56,7.56,69
2022-03-08,03:00:00,Rain,適度な雨,5.55,5.55,5.55,89
2022-03-08,06:00:00,Rain,小雨,4.87,4.87,4.87,87
2022-03-08,09:00:00,Rain,小雨,4.76,4.76,4.76,86
2022-03-08,12:00:00,Clouds,厚い雲,6.36,6.36,6.36,76
2022-03-08,15:00:00,Clouds,曇りがち,9.18,9.18,9.18,58
2022-03-08,18:00:00,Clouds,薄い雲,9.5,9.5,9.5,53
2022-03-08,21:00:00,Clouds,薄い雲,7.75,7.75,7.75,60
transport-inputdata-22-1.csv
日付,行先,交通手段,,,片道/往復,金額
1,客先,バス,公園前,HOGE駅南口,1,345
1,客先,電車,FUGA線HOGE駅,PIYO駅,1,1234
4,客先,バス,公園前,HOGE駅南口,1,345
4,客先,電車,FUGA線HOGE駅,PIYO駅,1,1234
5,客先,バス,公園前,HOGE駅南口,1,345
5,客先,電車,FUGA線HOGE駅,PIYO駅,1,1234
6,客先,バス,公園前,HOGE駅南口,1,345
6,客先,電車,FUGA線HOGE駅,PIYO駅,1,1234
7,客先,バス,公園前,HOGE駅南口,1,345
7,客先,電車,FUGA線HOGE駅,PIYO駅,1,1234
11,客先,バス,公園前,HOGE駅南口,1,345
11,客先,電車,FUGA線HOGE駅,PIYO駅,1,1234
13,客先,バス,公園前,HOGE駅南口,1,345
13,客先,電車,FUGA線HOGE駅,PIYO駅,1,1234
18,客先,バス,公園前,HOGE駅南口,1,345
18,客先,電車,FUGA線HOGE駅,PIYO駅,1,1234
19,客先,バス,公園前,HOGE駅南口,1,345
19,客先,電車,FUGA線HOGE駅,PIYO駅,1,1234
24,客先,バス,公園前,HOGE駅南口,1,345
24,客先,電車,FUGA線HOGE駅,PIYO駅,1,1234
27,客先,バス,公園前,HOGE駅南口,1,345
27,客先,電車,FUGA線HOGE駅,PIYO駅,1,1234
31,客先,バス,公園前,HOGE駅南口,1,345
31,客先,電車,FUGA線HOGE駅,PIYO駅,1,1234

会社に交通費を申請するとき、Web上で入力する作業をスクリプトで自動化しており、その際に使用している独自フォーマットのデータです。[1]
※駅名、金額などはダミーの値に書き換えています。

実践例3. 合計値を取得

awkでは定番であろう代表的な集計処理です。
transport-inputdata-22-1.csvを使用します。

3-1. 交通費の合計を出力

3-1 実行コマンド
cat transport-inputdata-22-1.csv | awk -F, 'NR>1{sum+=$7}END{print sum}'
3-1 実行結果
18948

パターンをNR>1としているのでヘッダ行は含めず[2]、に2行目以降の7列目($7)の金額の値を変数sumに加算して、最後にENDブロックでsumの値を出力します。

ENDブロック

awkは行単位(通常は改行文字区切り)で入力を読み取り、行毎にパターンを判定して該当するアクションを行うというのが基本的な動作です。合計値の計算は行毎に数値を加算しますが、合計値の出力は最後に一回出力すれば良いです。このようなケースでENDブロックが活躍します。

3-2. バスと電車それぞれで交通費を出力

3-1の例だけだとありきたりなので、もう1つ紹介します。

3-2 実行コマンド
cat transport-inputdata-22-1.csv | awk -F, '$3=="バス"{sum_bus+=$7}$3=="電車"{sum_train+=$7}END{print "バス:"sum_bus;print "電車:"sum_train;print "合計:"sum_bus+sum_train}'
3-2 実行結果
バス:4140
電車:14808
合計:18948

$3の値がバスか電車かで条件分岐して、別々の変数sum_bus,sum_trainに金額を加算し、最後にそれぞれの合計及び合計を出力しています。変数を複数用意して、パターンに応じて別々の変数を使うというテクニックです。
この例では条件分岐の種類はバス、電車の2種類のみですが、分岐の項目が多くなってきた場合にコードが長くなってしまいます。その場合は、実践例5で紹介するような連想配列for文を使用する方法が有効です。

実践例4. 階差を出力する

openweatherdata.csvを使用します。気温が3時間前(前の行)からどの程度変化したかを調べて、増分をフィールドとして追加して出力するといったことをやってみます。
また、フィールドが多いので特定のフィールドのみを出力することにします。列の追加・列の削除・列の交換など、awkはcsvの加工にも適しています。

4 実行コマンド
cat openweatherdata.csv | awk -F, -v OFS=, 'NR==1{$9="増分"}NR>1{$9=NR==2?"---":$5-tmp; tmp=$5}{print $1,$2,$3,$5,$9}' | column -s, -t
4 実行結果
日付        時刻      天気    気温   増分
2022-03-05  03:00:00  Clouds  8.21   ---
2022-03-05  06:00:00  Clouds  8.09   -0.12
2022-03-05  09:00:00  Clouds  9.51   1.42
2022-03-05  12:00:00  Clear   13.28  3.77
2022-03-05  15:00:00  Clear   17.62  4.34
2022-03-05  18:00:00  Clear   15.99  -1.63
2022-03-05  21:00:00  Clear   13.77  -2.22
2022-03-06  00:00:00  Clear   9.61   -4.16
2022-03-06  03:00:00  Clouds  7.41   -2.2
2022-03-06  06:00:00  Clouds  7.31   -0.1
2022-03-06  09:00:00  Clouds  9.04   1.73
2022-03-06  12:00:00  Clouds  10.71  1.67
2022-03-06  15:00:00  Clouds  10.88  0.17
2022-03-06  18:00:00  Clouds  8.24   -2.64
2022-03-06  21:00:00  Clouds  7.25   -0.99
2022-03-07  00:00:00  Clear   6.7    -0.55
2022-03-07  03:00:00  Clear   5.89   -0.81
2022-03-07  06:00:00  Clear   5.49   -0.4
2022-03-07  09:00:00  Clouds  7.92   2.43
2022-03-07  12:00:00  Clouds  10.48  2.56
2022-03-07  15:00:00  Clouds  11.74  1.26
2022-03-07  18:00:00  Clouds  9.66   -2.08
2022-03-07  21:00:00  Rain    8.2    -1.46
2022-03-08  00:00:00  Rain    7.56   -0.64
2022-03-08  03:00:00  Rain    5.55   -2.01
2022-03-08  06:00:00  Rain    4.87   -0.68
2022-03-08  09:00:00  Rain    4.76   -0.11
2022-03-08  12:00:00  Clouds  6.36   1.6
2022-03-08  15:00:00  Clouds  9.18   2.82
2022-03-08  18:00:00  Clouds  9.5    0.32
2022-03-08  21:00:00  Clouds  7.75   -1.75
2022-03-09  00:00:00  Clouds  7.33   -0.42
2022-03-09  03:00:00  Clouds  6.47   -0.86
2022-03-09  06:00:00  Clouds  5.98   -0.49
2022-03-09  09:00:00  Clouds  7.6    1.62
2022-03-09  12:00:00  Clouds  9.82   2.22
2022-03-09  15:00:00  Clouds  11.26  1.44
2022-03-09  18:00:00  Clouds  11.11  -0.15
2022-03-09  21:00:00  Clouds  10.35  -0.76
2022-03-10  00:00:00  Clouds  9.6    -0.75

以下にインデント・注釈を付けました。

awkコード
NR==1{
    # ヘッダ追加
    $9="増分"
}
NR>1{
    # 2行目は比較対象がないので"---"とし、それ以降は前行との差分を設定する
    $9=NR==2?"---":$5-tmp;
    # 次行で前行の値を取得するためにtmpに気温の値を格納する
    tmp=$5
}
{
    print $1,$2,$3,$5,$9
}

三項演算子: A ? B : C

人によっては見慣れない表記かと思うので念のため説明をします。

$9=NR==2?"---":$5-tmp;

この記述法は三項演算子という機能を利用しています。条件分岐を1文で簡潔に記述する方法で、以下のif文による条件分岐のコードと同値になります。

if(NR==2){
    $9="---"
}else{
    $9=$5-tmp
}

C言語やJavaなど多くのプログラミング言語が、同様の書式で三項演算子の機能をサポートしています。[3]

一般には、以下の書式になります。

実践例5: グループごとに平均値を出力

openweatherdata.csvから、日付毎の平均気温を出力します。
ここでは、このシリーズ初のfor文が登場します。

5 実行コマンド
cat openweatherdata.csv | awk -F, 'NR>1{cnt[$1]++;sum[$1]+=$5}END{for(i in cnt){print i,sum[i]/cnt[i]}}' | sort
5 実行結果
2022-03-05 12.3529
2022-03-06 8.80625
2022-03-07 8.26
2022-03-08 6.94125
2022-03-09 8.74
2022-03-10 9.6

以下にインデント・注釈を付けました。

awkコード
# 最初(NR:1)はヘッダなので無視。
NR>1{
    # 平均を出すために個数が必要なので、日付をキーとしてcntを+1する
    cnt[$1]++;
    # 気温の値($5)を、日付をキーとしてsumに加算。
    sum[$1]+=$5
}

# 出力はENDブロックでまとめて行う
END{
    # 範囲for文で日付を取り出し、日付と 平均(=合計/個数) を出力する
    for(i in cnt){
        print i,sum[i]/cnt[i]
    }
}

このように、特定のフィールドをキーとして連想配列に保持しておき、ENDブロックでfor文を利用して取り出すといったテクニックは頻繁に利用する気がします。連想配列を上手く利用することで、SQLなどで言うところのgroup byに近いような動作も可能です。
awkを始め連想配列は順序を保持しない場合が多く、今回は日付の昇順にするために最後に| sortを入れています。

出力結果にヘッダを出力したい場合は、以下のようにawkコマンドのBEGINブロックに記述するとスリムになると思います。

ヘッダ出力
BEGIN{print "日付","平均気温"}

または、以下のようにsedのiコマンドを用いていも良いです。実行コマンドにsedコマンドを繋げた例を以下に記述します。

5 実行コマンド ヘッダ付き
cat openweatherdata.csv | awk -F, 'NR>1{cnt[$1]++;sum[$1]+=$5}END{for(i in cnt){print i,sum[i]/cnt[i]}}' | sort | sed '1i 日付 平均気温'

sed 'ni テキスト'
とすれば、標準入力で受け取ったテキストのn行目の↑に文字列テキストを挿入することが出来ます。

まとめ

今回は集計処理をメインに紹介しました。Linux上で集計を行うコマンドは色々ありますが、そのなかでもawkはとても手軽にこなすことが出来ます。

実践例3-1の処理はawkを使わずに処理するならば、文字列を整形して計算式にしてそこからbcに渡すといった方法が考えられます。以下はsedbcを用いた例です。[4]

実践例3-1 sedとbcを用いた例
cat transport-inputdata-22-1.csv | sed 1d | sed -E 's/.*,([^,]*)$/\1/' | sed -z 's/\n/+/g;s/+$/\n/' | bc

sedを3回組み合わせていますが、それぞれ簡単に解説をつけます。

コマンド オプション 説明
1d 無し 1行目(ヘッダ)を削除
s/.*,([^,]*)$/\1/ -E ,区切りで最後の列のみを取り出す
正規表現の後方参照を利用
s/\n/+/g;s/+$/\n/ -z ;区切りで2つの置換コマンドが入っています
1つ目の置換で改行コードを+に変更してから
2つ目の置換で最後尾の+は改行コードに変換[5]

このワンライナーは、sedの独特な書式や正規表現に慣れていないと処理が難しいかもしれません。また、より複雑な書式のファイル、より複雑な操作になった際には対応が難しくなるでしょう。例えば実践例5のようにグループごとに平均値を出力などのようになると、同じような方法は通用しないでしょう。

もちろんLinuxコマンドで行うことを諦め、Pythonなどの数値計算が得意なスクリプト言語に任せてしまうのも手だと思います。筆者もawkを使う前はこのような処理はすべてPythonを使って処理していました。

awkがある程度使えるようになると、この程度の計算にわざわざスクリプトファイルを書く必要がなくなるので、そういう面でもawkの利用価値を感じています!

相変わらず上手くまとまらないですね。次回のテーマははっきりと決めていないですが、awkとgrep/sed等のテキストフィルタリングコマンドを対比しながら魅力を伝えられればと思います。先にsed/grep等の記事を書くかもしれません。

このシリーズを一通り執筆し終えたら、「本」という形でまとめられるといいなぁと考えています。

脚注
  1. 本当はこの形式のままアップロードが出来ればいいのですが、弊社ではWeb上で1つ1つ手作業で入力しなければならず面倒なので…、許可を取ってPython/Seleniumによる入力自動化をさせてもらっています。 ↩︎

  2. 実はこの例に関してはNR>1は不要です。ヘッダが数字でなければsumに足しても0として扱われるみたいですが、ヘッダを含むcsvを扱う場合は癖としてNR>1を書くようにした方が良いと思います。 ↩︎

  3. 便利な三項演算子ですが、ネストをしたり条件や値などが長くなったり複雑になるようなものは、可読性を落とすことからプログラム中で積極的に使用されることが少ないように思います。実践例4のようにシンプルな用途で使用するのが吉だと思います。 ↩︎

  4. 筆者はsedに慣れているので大抵文字列の整形にはsedを使いがちなのですが、多分もっと良い方法はあると思います。 ↩︎

  5. 2つ目のコマンドはbcコマンドに渡す際に最後が改行コードになっていないとエラーになってしまうため。2つ目はs/$/0\n/としてもいいですね。 ↩︎

GitHubで編集を提案

Discussion