【GAWK】でGROUP BY、カウント/合計、PIVOT【CSV】

GAWKで PIVOT

グーグルスプレッドシートの SQLライクな QUERY関数をいじくり回していたらなんとな~く GROUP BYとか PIVOTとかがイメージできてきた。
GROUP BYは uniqなのね。
PIVOT部はそこに辞書(JSONで言うオブジェクト)が入ってるようなものなのね。
{PIVOT1:集計値1, PIVOT2:集計値2, ...}
なら GAWKの連想配列でできそうな気がしてきた。

サンプルデータとして
東京都 新型コロナウイルス陽性患者発表詳細 - データセット - 東京都オープンデータカタログサイト

↑から
東京都_新型コロナウイルス陽性患者発表詳細(全期間一括版)
130001_tokyo_covid19_patients.csv
の先頭23行を抜き出したものを用意する。

sample.csv↓

No,全国地方公共団体コード,都道府県名,市区町村名,公表_年月日,発症_年月日,確定_年月日,患者_居住地,患者_年代,患者_性別,患者_職業,患者_状態,患者_症状,患者_渡航歴の有無フラグ,患者_接触歴の有無フラグ,備考,退院済フラグ
1,130001,東京都,,2020-01-24,,,湖北省武漢市,40代,男性,,,,,,,1
2,130001,東京都,,2020-01-25,,,湖北省武漢市,30代,女性,,,,,,,1
3,130001,東京都,,2020-01-30,,,湖南省長沙市,30代,女性,,,,,,,1
4,130001,東京都,,2020-02-13,,,都内,70代,男性,,,,,,,1
5,130001,東京都,,2020-02-14,,,都内,50代,女性,,,,,,,1
6,130001,東京都,,2020-02-14,,,都内,70代,男性,,,,,,,1
7,130001,東京都,,2020-02-15,,,都内,80代,男性,,,,,,,1
8,130001,東京都,,2020-02-15,,,都内,50代,女性,,,,,,,1
9,130001,東京都,,2020-02-15,,,都内,50代,男性,,,,,,,1
10,130001,東京都,,2020-02-15,,,都内,70代,男性,,,,,,,1
11,130001,東京都,,2020-02-15,,,都内,70代,男性,,,,,,,1
12,130001,東京都,,2020-02-15,,,都内,40代,男性,,,,,,,1
13,130001,東京都,,2020-02-15,,,都内,60代,女性,,,,,,,1
14,130001,東京都,,2020-02-15,,,都内,40代,男性,,,,,,,1
15,130001,東京都,,2020-02-16,,,都内,60代,男性,,,,,,,1
16,130001,東京都,,2020-02-16,,,都内,30代,男性,,,,,,,1
17,130001,東京都,,2020-02-16,,,都内,60代,男性,,,,,,,1
18,130001,東京都,,2020-02-16,,,都外,60代,男性,,,,,,,1
19,130001,東京都,,2020-02-16,,,都内,30代,男性,,,,,,,1
20,130001,東京都,,2020-02-18,,,都内,80代,男性,,,,,,,1
21,130001,東京都,,2020-02-18,,,都内,20代,男性,,,,,,,1
22,130001,東京都,,2020-02-18,,,都内,50代,男性,,,,,,,1

この CSVに
=QUERY(A1:Q23, "SELECT C, E, COUNT(A) GROUP BY C, E PIVOT J, I", 1)
相当の操作をする GAWKスクリプトを考える。
まず $3 $5 $10 $9 の組み合わせを数えるために group_by[$3, $5][$10 "_" $9]++ していき、
最後に一行毎、group_by[$3, $5]毎に group_by[$3, $5][$10 "_" $9]の値を並べていく。

cat sample.csv | gawk -e '
BEGIN {
  FS = ","
  OFS = ","
  SUBSEP = OFS
  ORS = ""
  #PROCINFO["sorted_in"] = "@ind_num_asc"
}
        #MAIN.
NR==1{print $3, $5}  #ヘッダ前半
NR>1{group_by[$3, $5][$10 "_" $9]++}
NR>1{pivot_headers_dic[$10 "_" $9] = 1}

END{
          #ピボット列名を「配列」に.
  asorti(pivot_headers_dic, pivot_headers, "@ind_str_asc")

          #ヘッダ後半.
  for (idx in pivot_headers){
    pivot = pivot_headers[idx]
    print OFS pivot
  }
  print "\n"

          #グループ部を一行ずつ.
  for (group in group_by){
    line = group
            #その一行にピボット列の値を追加していく.
    for (idx in pivot_headers){
      pivot = pivot_headers[idx]
      line = line OFS sprintf("%3s", group_by[group][pivot])
    }       #→一行完成
    print line "\n" | "sort"
  }
}
' #>outfile.csv

出力は以下のとおり↓

都道府県名,公表_年月日,女性_30代,女性_50代,女性_60代,男性_20代,男性_30代, 男性_40代,男性_50代,男性_60代,男性_70代,男性_80代
東京都,2020-01-24,   ,   ,   ,   ,   ,  1,   ,   ,   ,
東京都,2020-01-25,  1,   ,   ,   ,   ,   ,   ,   ,   ,
東京都,2020-01-30,  1,   ,   ,   ,   ,   ,   ,   ,   ,
東京都,2020-02-13,   ,   ,   ,   ,   ,   ,   ,   ,  1,
東京都,2020-02-14,   ,  1,   ,   ,   ,   ,   ,   ,  1,
東京都,2020-02-15,   ,  1,  1,   ,   ,  2,  1,   ,  2,  1
東京都,2020-02-16,   ,   ,   ,   ,  2,   ,   ,  3,   ,
東京都,2020-02-18,   ,   ,   ,  1,   ,   ,  1,   ,   ,  1

うまくいった(もちろんここまで大変だった^^)のでもう少し汎用性を高めてテンプレートにして作り置き。

CSVデータを GROUP BYしてカウントか合計を PIVOTするテンプレート

        #csv, group and pivot by gawk.

        #パラメタ設定始め.
CSVFILE='sample.csv'
GROUP='$3, $5'  #グループ化するフィールドをカンマ区切りで
#GROUP='""'  #1行になる
PIVOT='$10 "_" $9'  #ピボットしてヘッダになるフィールド(を文字列結合する式)
#PIVOT='""'  #グループ化のみ
CALCULATOR='++'  #カウント
#CALCULATOR='+=$1'  #$1の合計
FORMAT='%3s'  #値の書式
ROWSORT=''    #行ソート、文字順
#ROWSORT='-n'  #行ソート、数字順
#ROWSORT='-nr'  #行ソート、数字逆順
COLSORT='@ind_str_asc'    #ピボット列ソート、文字順
#COLSORT='@ind_str_desc'  #ピボット列ソート、文字逆順
#COLSORT='@ind_num_asc'  #ピボット列ソート、数字順
OUTFILE='/dev/stdout'
#OUTFILE='outfile.csv'
        #パラメタ設定終わり.

cat $CSVFILE | gawk -f <(cat <<__HERE__
BEGIN {
  FS = ","
  OFS = ","
  SUBSEP = OFS
  ORS = ""
  #PROCINFO["sorted_in"] = "@ind_num_asc"
}
        #MAIN.
NR==1{print $GROUP}  #ヘッダ前半
NR>1{group_by[$GROUP][$PIVOT]$CALCULATOR}
NR>1{pivot_headers_dic[$PIVOT] = 1}

END{
          #ピボット列名を「配列」に.
  asorti(pivot_headers_dic, pivot_headers, "$COLSORT")

          #ヘッダ後半.
  for (idx in pivot_headers){
    pivot = pivot_headers[idx]
    print OFS pivot
  }
  print "\n"

          #グループ部を一行ずつ.
  for (group in group_by){
    line = group
            #その一行にピボット列の値を追加していく.
    for (idx in pivot_headers){
      pivot = pivot_headers[idx]
      line = line OFS sprintf("$FORMAT", group_by[group][pivot])
    }       #→一行完成
    print line "\n" | "sort $ROWSORT"
  }
}
__HERE__
) >$OUTFILE

上記のパラメタで実行すると出力はもちろん上と同じになる。
では元サンプルからグループ化列は同じでピボット列を患者_年代=第9列にして
PIVOT = '$9'
として実行すると出力は以下のようになる。
=QUERY(A1:Q23, "SELECT C, E, COUNT(A) GROUP BY C, E PIVOT I", 1) に相当↓

都道府県名,公表_年月日,20代,30代,40代,50代,60代,70代,80代
東京都,2020-01-24,   ,   ,  1,   ,   ,   ,
東京都,2020-01-25,   ,  1,   ,   ,   ,   ,
東京都,2020-01-30,   ,  1,   ,   ,   ,   ,
東京都,2020-02-13,   ,   ,   ,   ,   ,  1,
東京都,2020-02-14,   ,   ,   ,  1,   ,  1,
東京都,2020-02-15,   ,   ,  2,  2,  1,  2,  1
東京都,2020-02-16,   ,  2,   ,   ,  3,   ,
東京都,2020-02-18,  1,   ,   ,  1,   ,   ,  1

ではグループ化列を都道府県名、公表_年月日、患者_性別=第3, 5, 10列にして、ピボット列を患者_年代=第9列にして
GROUP = '$3, $5, $10'
PIVOT = '$9'
として実行すると出力は以下のようになる。
=QUERY(A1:Q23, "SELECT C, E, J, COUNT(A) GROUP BY C, E, J PIVOT I", 1) に相当↓

都道府県名,公表_年月日,患者_性別,20代,30代,40代,50代,60代,70代,80代
東京都,2020-01-24,男性,   ,   ,  1,   ,   ,   ,
東京都,2020-01-25,女性,   ,  1,   ,   ,   ,   ,
東京都,2020-01-30,女性,   ,  1,   ,   ,   ,   ,
東京都,2020-02-13,男性,   ,   ,   ,   ,   ,  1,
東京都,2020-02-14,女性,   ,   ,   ,  1,   ,   ,
東京都,2020-02-14,男性,   ,   ,   ,   ,   ,  1,
東京都,2020-02-15,女性,   ,   ,   ,  1,  1,   ,
東京都,2020-02-15,男性,   ,   ,  2,  1,   ,  2,  1
東京都,2020-02-16,男性,   ,  2,   ,   ,  3,   ,
東京都,2020-02-18,男性,  1,   ,   ,  1,   ,   ,  1

では全く無意味だが No=第1列を加算してみよう。
GROUP = '$5'
PIVOT = '$10'
CALCULATOR = '+=$1'
として実行すると出力は以下のようになる。
=QUERY(A1:Q23, "SELECT E, SUM(A) GROUP BY E PIVOT J", 1) に相当↓

公表_年月日,女性,男性
2020-01-24,   ,  1
2020-01-25,  2,
2020-01-30,  3,
2020-02-13,   ,  4
2020-02-14,  5,  6
2020-02-15, 21, 63
2020-02-16,   , 85
2020-02-18,   , 63

う~ん、無意味だ(笑
では
PIVOT = '""'
CALCULATOR='++'
だと単純に毎日の人数が分かる。
=QUERY(A1:Q23, "SELECT E, COUNT(A) GROUP BY E", 1) に相当↓

公表_年月日,
2020-01-24,  1
2020-01-25,  1
2020-01-30,  1
2020-02-13,  1
2020-02-14,  2
2020-02-15,  8
2020-02-16,  5
2020-02-18,  3

そして
GROUP='$3'
PIVOT='$10 "_" $9'
だと都道府県別小計になる…ってそもそも東京都しかないけど^^
=QUERY(A1:Q23, "SELECT C, COUNT(A) GROUP BY C PIVOT J, I", 1) に相当↓

都道府県名,女性_30代,女性_50代,女性_60代,男性_20代,男性_30代,男性_40代,男 性_50代,男性_60代,男性_70代,男性_80代
東京都,  2,  2,  1,  1,  2,  3,  2,  3,  4,  2

ならば年別月別小計ができそうなものだが惜しいかな、年や月が単独のフィールドになっていない。
もちろん事前に AWKで、

awk '
BEGIN{FS=",";OFS=","}
NR==1{sub("年月日", "年,月,日", $5);print}
NR>1{gsub("-", ",", $5);print}
' sample.csv

ハイフンをカンマにしておけば GROUP='$5' で年別、GROUP='$5, $6' で月別小計が出せるのだが
(後ろのフィールド番号がずれるので注意)。

おわりに

「QUERY関数でできるならスプレッドシートでいいのでは?」

いやあ、17列×100万レコード超え(千万セル超え)で元の東京都の CSVは開けないんですよね。
GAWKなら一瞬…いや、3拍くらいかな^^
まあ、何でも入力が一番たいへんなんですが。
コロナ対応のみな様ご苦労様です。

(おしまい)


主な修正履歴。
2022-04-26: 最後のコードブロックを修正。

この記事が気に入ったらサポートをしてみませんか?