【シェルと遊ぼう】日本のコロナ感染者情報取得スクリプト その5(確定日別の集計)

はじめに

こちらの記事の続き。

元々前回で終わる予定だったのだけれども、前回までのやり方では、
元データ(https://jag-japan.com/covid19map-readme)から退院数や死亡者数などが正しく集計できそうにない。
そこで、退院数や死亡者数を集計結果を出力する専用オプションを追加する。

作るもの

前回は4まで。

  1. データベースの作成・更新と全行全項目の出力
  2. 全感染者の指定項目を取得 (列の抽出,並べ替え)
  3. 指定項目の条件でフィルタ (行の抽出)
  4. 指定項目の内訳集計 (出現回数のカウント)
  5. 確定日別の集計 (集計用のDB追加する) <== New

以降では、前回同様作るスクリプト名をcovidとした前提で説明する。

実行イメージ

$ covid -t
確定日,感染者数累計,感染者数前日比,死者合計,退院数累計,退院数前日比,PCR検査実施人数,PCR検査実施人数前日比
2020/01/15,1,1,,1,1,,
2020/01/24,2,1,,,,,
2020/01/25,3,1,,,,,
 ...
 ...
2020/05/14,16175,49,687,10338,470,233144,5700
2020/05/15,16184,9,,,,,

-tオプション未指定時と同様、条件指定や列指定もできる。

$ covid -t -m 'date~"2020/05"' date inf die leav pcr
確定日,感染者数累計,死者合計,退院数累計,PCR検査実施人数
2020/05/01,14740,432,3981,174510
2020/05/02,15044,458,4211,181527
2020/05/03,15228,492,4385,183251
 ...
2020/05/13,16126,668,9868,223667
2020/05/14,16175,687,10338,233144
2020/05/15,16184,,,

列項目について

今回の-tオプション指定時は、指定できる列項目が下表のパラメータになる。

項目 パラメータ 備考
確定日 date PCR検査の陽性確定日
感染者数累計 inf 感染者数の累計
感染者数前日比 infd 感染者数の前日比
死者合計 die 死亡者の累計
退院数累計 leav 退院者数の累計
退院数前日比 leavd 退院者数の前日比
PCR検査実施人数 pcr PCR検査実施人数の累計
PCR検査実施人数前日比 pcrd PCR検査実施人数の累計。パット見で数が合ってない箇所も。。

確定日以外の出力は、空欄か数字(人数)のみ。
空欄は元データが空欄のもの。
公表待ちの他にもありそうで、理由がよく分からないので、本スクリプトでも空欄のまま出力する。

コード

処理的には前回までの一連の処理と同じ。
ただDBファイルや列項目名が変わっただけ。

コード全体はこちらを参照されたし。

DBファイル作成

集計用のDBファイルのパスは以下。

DB_DIR="${XDG_DATA_HOME:-${HOME}/.local/share/}/${CMD_NAME}"
# 〜中略〜
TOTAL_DB_FILE="${DB_DIR}/TOTAL_DB_COVID-19.csv"

元データのCSVファイルから取得する列位置と出力ヘッダ名は以下。

TOTAL_DB_ITEMS='$40,$24,$25,$27,$28,$29,$30,$31'
#TOTAL_DB_HEADER='date,inf,infd,die,leav,leavd,pcr,pcrd'
TOTAL_DB_HEADER='確定日,感染者数累計,感染者数前日比,死者合計,退院数累計,退院数前日比,PCR検査実施人数,PCR検査実施人数前日比'

集計用のDBファイルが無かったら作成する。

# make database for total count
if [ ! -f "$TOTAL_DB_FILE" ]; then
  echo "$TOTAL_DB_HEADER" > "${TOTAL_DB_FILE}"
  cat "$RAW_DB_FILE"    \
    | make_total_database >> "${TOTAL_DB_FILE}"
fi

実際に作成している関数は以下。
感染者数累計が空欄か#REF!の行は省いている。
また、確定日の最終出現行のみ出力させる為、一旦降順でソートして、最後に元に戻している。

make_total_database(){
  get_body_recode   \
    | delete_space  \
    | format_date   \
    | sort -t ',' -k 1 -nr \
    | awk -F"," \
    '   BEGIN{ OFS="," }
        $24!="" && $24!="#REF!" && !uniq[$40]++ {
          print '"$TOTAL_DB_ITEMS"'
        }'      \
    | sort -t ',' -k 1
}

!uniq[$40]++で、日付の重複を避けている。
awkではゼロはFALSEとなる。
論理反転しているのでゼロのときはTRUE、ゼロ以外がFALSEとなる。
つまり2020/5/15が複数回出てきた場合、最初に出てくる2020/5/15の行のときだけ条件が成立する。

そして、awkに渡す前に降順でソートしているので、元データのCSVファイル上で、
最後に出てきた2020/5/15の行にだけマッチするという想定。

ただ、報告漏れなどがあって後からデータが追加される場合、
元データのCSVがどう修正されていくか分からない。。。

全行上からチェックして空文字じゃなければ上書き更新とした方が安全かもしれない。
その場合、列数分だけ配列が必要になりそう。
すぐできそうなので気になる人はやってみてねと。

列名チェック

-tオプション指定時の列項目名チェック。
怠いコードをそのまま増産。

if [ -n "$TOTAL_COUNT" ]; then
  for field in "$@";do
    case $field in
    date | inf | infd | die | leav | leavd | pcr | pcrd ) ;;
    * ) ERR_MSG="invalid field-name '$field'" ;;
    esac
  done
else

列項目名から列位置への置き換え

-tオプション指定時は、参照するDBファイルと項目名チェックに使う関数を切り替える。

if [ -n "$TOTAL_COUNT" ]; then
  NAME_TO_POS=total_name_to_pos
  DB_FILE="$TOTAL_DB_FILE"
fi

チェックに使う関数も怠いコードをそのまま増産。

total_name_to_pos(){
  sed -e 's/date/$1/g' \
    -e 's/infd/$3/g'   \
    -e 's/inf/$2/g'    \
    -e 's/die/$4/g'    \
    -e 's/leavd/$6/g'  \
    -e 's/leav/$5/g'   \
    -e 's/pcrd/$8/g'   \
    -e 's/pcr/$7/g'
}

ただ、置き換える順番には注意。
infdより先にinfで置き換えるとinfd$2dとかになっちゃう。

出力

出力するコードは今までと一緒。
条件を絞る処理なんかも今までと共通。

PICKUP_COND=`echo $PICKUP_COND | $NAME_TO_POS \
  | sed 's/\([^!~><=]\)=\([^=]\)/\1==\2/g'`
if [ -n "$CNT_IN_FIELD" ]; then
  CNT_IN_FIELD=`echo $CNT_IN_FIELD | $NAME_TO_POS`
  # output count each of field
  cat "$DB_FILE"    \
    | sed '1d'      \
    | awk -F"," '
      BEGIN{ OFS="," }
      '"$PICKUP_COND"'{
        count['$CNT_IN_FIELD']++
      }
      END{
        total=0
        for (field in count) {
          print field,count[field]
          total+=count[field]
        }
        print ":TOTAL",total
      }'
elif [ -n "$CNT_OF_RECODE" ]; then
  # output count of pickup recodes
  cat "$DB_FILE"    \
    | sed '1d'      \
    | awk -F"," '
      BEGIN{ count=0 }
      '"$PICKUP_COND"'{
        count++
      }
      END{ print count }'
else
  # output pickup headr
  head -n 1 "$DB_FILE"  \
    | awk -F"," '
      BEGIN{ OFS="," }
      {
        print '"${OUTPUT_ITEMS}"'
      }'

  # output pickup recodes
  cat "$DB_FILE"    \
    | sed '1d'      \
    | awk -F"," '
      BEGIN{ OFS="," }
      '"$PICKUP_COND"'{
        print '"${OUTPUT_ITEMS}"'
      }'
fi

さいごに

今回のスクリプトはawkさまさまです。
表計算も楽々ですな。やらんけど。

ただ、初回の記事にも書いたけど、僕のようにシェルスクリプトを書くことが目的じゃなければ、
他のサイトさんのデータを使った方が楽だしデータの更新頻度や精度も良いかも。

ちなみに、かの有名なmattnさんなんて下のようなものを作ってらっしゃる。

まるで息を吐くかの如く VimScript や Goプログラム を量産されるお方ですな。スゴイ。

ということで、これでおしまい。
もうやらなーい。

確認環境

PC Thinkpad X1 Carbon 2nd Gen
OS FreeBSD 12.1-RELEASE-p4

参考

【シェルと遊ぼう】日本のコロナ感染者情報取得スクリプト その4(指定列項目の内訳集計)

はじめに

こちらの記事の続き。

作るもの

今回は指定列の条件でフィルタリング処理について。

  1. データベースの作成・更新と全行全項目の出力
  2. 全感染者の指定項目を取得 (列の抽出,並べ替え)
  3. 指定項目の条件でフィルタ (行の抽出)
  4. 指定項目の内訳集計 (出現回数のカウント)    <== 今回はこれ

以降では、前回同様作るスクリプト名をcovidとした前提で説明する。

実行イメージ

使い方と出力は以下みたいな感じ。

指定列項目の値と、その値の出現回数をカンマ区切りで出力する。
最終行の':TOTAL'は総数を算出して追加で出力している。
ご覧の通り、ソートは無し。外部プログラムにお任せ。

$ covid -f age          # 年代別内訳
70,1541
40,2535
90,480
不明,342
10,365
60,1803
100,10
30,2368
80,1104
50,2656
0,256
20,2594
:TOTAL,16054

-mオプションと併用できますとも。

$ covid -f area1 -m 'age<10'     # 10歳未満の感染者の受診都道府県別内訳
香川県,3
宮城県,6
新潟県,3
福島県,2
石川県,3
東京都,66
山形県,2
奈良県,3
愛媛県,2
京都府,4
北海道,14
長野県,2
三重県,4
不明,1
秋田県,1
富山県,7
群馬県,1
茨城県,2
福岡県,14
島根県,3
岐阜県,4
兵庫県,10
千葉県,7
高知県,4
山口県,1
静岡県,4
神奈川県,15
山梨県,2
埼玉県,13
愛知県,15
和歌山県,1
大阪府,30
福井県,2
滋賀県,5
:TOTAL,256

列項目について

前回も載せたけど、指定可能な列項目は以下。

項目 パラメータ 備考 出力例
ID id データの通し番号。元データの「通し」 1/2/3...
確定日 date PCR検査の陽性確定日 '2020/05/13'など
受診都道府県 area1 陽性確定時に感染者が受診していた医療機関がある都道府県。空港名もある... 東京都/沖縄県/不明など
居住地 area2 感染者の住居都道府県または居住国 東京都/沖縄県/不明など
居住地(詳細) area3 元データ項目では「キー」。居住都道府県と同じデータも多数 東京都/沖縄県那覇市/不明など
年代 age 感染者の年代。元データは100歳以上も90に含むっぽい 0/10/20/30/40/50/60/70/80/90/100/不明
性別 sex 感染者の性別 男性/女性/不明
職業 job 感染者の職業。記載がある件数が少なく同職種でも記載に統一感なし... 会社員/看護師/無職/不明(#1)など
備考 note 詳細地区や年齢やその他補足情報っぽい 足立区/100歳以上/再陽性
状態 sts 感染者の状態。元データの「ステータス」と「無症状病原体保有者」の組み合わせ 死亡/退院/退院または死亡(国内無症状)/空欄

年代については元データをスクリプトで以下のように加工して出力する。

元データ 出力 備考
0-10 0 10歳未満
90 100 備考に「100歳以上」と記載がある場合のみ
空欄 不明

コード

列内の集計に関する主要コードは以下。
コード全体はこちらを参照されたし。

引数処理

受け取った引数を変数に入れておく。

CNT_IN_FIELD=
# 〜中略〜
while getopts cf:hm:tuV OPT
do
  case $OPT in
    # 〜中略〜
    f ) CNT_IN_FIELD=$OPTARG ;;
    # 〜中略〜
  esac
done

この記事を書いてて気付いた。
この引数の項目名が正しいかチェックしてない。
なんてこった。

列項目名から列位置への置き換えと出力

下のコードで内訳集計結果を出力している。

まず、項目名からawkの列位置へ置き換えておく。

そして、ヘッダ行を削ったDBファイルのデータをawkに渡す。
awkでは、出てきた項目を配列のキーに登録して、同じキーをカウントしていく。

全行カウントが終わったら、全キーのカウント数と合計数を出力。

if [ -n "$CNT_IN_FIELD" ]; then
  CNT_IN_FIELD=`echo $CNT_IN_FIELD | $NAME_TO_POS`
  # output count each of field
  cat "$DB_FILE"    \
    | sed '1d'      \
    | awk -F"," '
      BEGIN{ OFS="," }
      '"$PICKUP_COND"'{
        count['$CNT_IN_FIELD']++
      }
      END{
        total=0
        for (field in count) {
          print field,count[field]
          total+=count[field]
        }
        print ":TOTAL",total
      }'

ちなみに、$NAME_TO_POSには出力する列項目で使っていた怠い関数が入っている。

item_name_to_pos(){
  sed -e 's/id/$1/g'    \
    -e 's/date/$2/g'    \
    -e 's/area1/$3/g'   \
    -e 's/area2/$4/g'   \
    -e 's/area3/$5/g'   \
    -e 's/age/$6/g'     \
    -e 's/sex/$7/g'     \
    -e 's/job/$8/g'     \
    -e 's/note/$9/g'    \
    -e 's/sts/$10/g'
}

もっと面倒になるかと思ったけれども、やっぱりawkスゴイ。

さいごに

最初の記事に書いたのだけども、元データのCSVの「ステータス」列は空欄が多い。
その為、今回のオプションを利用して退院数や死亡者数などを集計するのは無理だった。
数値が全く合わない。

元データのサイトさんはどうやって集計しているのかというと、
死亡者数や退院者数は別の列として用意している。
そして、出現する確定日の最終行にのみ国が公表した数値を入れてるっぽい。
以下のようなイメージ。

確定日,・・・,死亡者数累計,退院累計,・・・
 ...
2020/5/13,・・・,,,・・・
2020/5/13,・・・,,,・・・
2020/5/13,・・・,,,・・・
2020/5/13,・・・,668,20,・・・
2020/5/14,・・・,,,・・・
2020/5/14,・・・,,,・・・
2020/5/14,・・・,687,10338,・・・
2020/5/15,・・・,,,・・・
 ...

仕方がないので、これを利用して確定日別の集計表を作成しよう。

というのはまた次回。

確認環境

PC Thinkpad X1 Carbon 2nd Gen
OS FreeBSD 12.1-RELEASE-p4

参考

【シェルと遊ぼう】日本のコロナ感染者情報取得スクリプト その3(条件による行の抽出)

はじめに

こちらの記事の続き。

作るもの

今回は指定した条件でフィルタリングする処理について。

  1. データベースの作成・更新と全行全項目の出力
  2. 全感染者の指定項目を取得 (列の抽出,並べ替え)
  3. 指定項目の条件でフィルタ (行の抽出)      <== 今回はこれ
  4. 指定項目の内訳集計 (出現回数のカウント)

以降では、前回同様作るスクリプト名をcovidとした前提で説明する。

実行イメージ

使い方は以下みたいな感じ。

-mオプションの引数に条件式を渡して、条件にマッチした行だけ出力する。
条件は全体をシングルクォーテーションで囲う必要あり。

条件式は、項目名だけ列位置に置き換えてawkにそのまま渡す。
つまりawkの条件式がそのまま使えちゃう手抜きステキ仕様。

$ covid -m 'age<20' date area2 age sex            # 20代未満の感染者のみ取得
確定日,居住地,年代,性別
2020/02/18,和歌山県,10,男性
2020/02/21,北海道,0,男性
2020/02/21,北海道,10,男性
...
...
2020/05/12,大阪府,0,女性
$ covid -m 'date~"2020/05" && area2=="東京都"'    # 2020年5月の東京都の感染者のみ取得
ID,確定日,受診都道府県,居住地,居住地(詳細),年代,性別,職業,備考,状態
14453,2020/05/01,東京都,東京都,東京都,70,女性,,,
14454,2020/05/01,東京都,東京都,東京都,70,女性,,,
14455,2020/05/01,東京都,東京都,東京都,70,女性,,,
...
...
16054,2020/05/13,東京都,東京都,東京都,不明,不明,,,
$ covid -m 'sts~"無症状"'                     # 症状がない感染者のみ取得
ID,確定日,受診都道府県,居住地,居住地(詳細),年代,性別,職業,備考,状態
10,2020/01/30,千葉県,中華人民共和国,中華人民共和国,50,女性,,,退院(チャーター無症状2)
16,2020/01/31,不明,不明,不明,30,男性,,,(チャーター無症状3)
19,2020/02/01,不明,不明,不明,30,男性,,,(チャーター無症状5)
...
...
16031,2020/05/12,大阪府,大阪府,大阪府大阪市,20,男性,医療従事者,,(国内無症状)

ただ、age=10など、===にして代入しちゃうミスはやりがち。
なので=単体は==に置き換える処理を追加。

件数カウント

条件を指定できるようになったら、その条件に引っかかった件数を数えたくなるもの。
grepとかwcでいいんだけど、すぐに実装できるのでしちゃう。

$ covid -c -m 'sts~"無症状"'                  # 症状がない感染者のみ取得
658

条件に引っかかった行があればcountをインクリメントしていき、最後に出力。

集計された行数と感染者数は国や自治体の発表とだいたい合ってそう。
元データのQ&Aによると、公表日を基準とするか、PCR検査での確定日を基準とするかで差異があるとのこと。

ただ各自治体によって、集計に使うのが診断都道府県か、居住都道府県で差異がありそうなんだけど、気のせいかしら。。

条件式で使える演算子など

前述だけど、awkで使える条件式を使える。
つまり、awk A{print $0}Aの部分に使う条件式を-m 'A'で指定する。

awkのパターンで使える代表的な演算子は以下。

演算子 意味
X==Y AとBは同値。本スクリプトではA=Bでも可
X!=Y AとBは同値
X>=Y AはB以上
X<=Y AはB以下
X>Y AはBより大きい
X\<Y AはBより小さい
X~Y Aは正規表現Bにマッチする
X!~Y Aは正規表現Bにマッチしない
!A 論理反転
A&&B AかつB
A |B |AまたはB

文字列はダブルクォーテーションで囲わないといけない。

全列を対象としたgrepっぽいことをするなら-m '/検索パターン/'とすればOK。

列項目について

前回も載せたけど、指定可能な列項目は以下。

項目 パラメータ 備考 出力例
ID id データの通し番号。元データの「通し」 1/2/3...
確定日 date PCR検査の陽性確定日 '2020/05/13'など
受診都道府県 area1 陽性確定時に感染者が受診していた医療機関がある都道府県。空港名もある... 東京都/沖縄県/不明など
居住地 area2 感染者の住居都道府県または居住国 東京都/沖縄県/不明など
居住地(詳細) area3 元データ項目では「キー」。居住都道府県と同じデータも多数 東京都/沖縄県那覇市/不明など
年代 age 感染者の年代。元データは100歳以上も90に含むっぽい 0/10/20/30/40/50/60/70/80/90/100/不明
性別 sex 感染者の性別 男性/女性/不明
職業 job 感染者の職業。記載がある件数が少なく同職種でも記載に統一感なし... 会社員/看護師/無職/不明(#1)など
備考 note 詳細地区や年齢やその他補足情報っぽい 足立区/100歳以上/再陽性
状態 sts 感染者の状態。元データの「ステータス」と「無症状病原体保有者」の組み合わせ 死亡/退院/退院または死亡(国内無症状)/空欄

年代については元データをスクリプトで以下のように加工して出力する。

元データ 出力 備考
0-10 0 10歳未満
90 100 備考に「100歳以上」と記載がある場合のみ
空欄 不明

コード

行の抽出に関する主要コードは以下。

コード全体はこちらを参照されたし。

条件の初期化と設定

-mオプションで渡された引数を変数にセットしておく。
初期値は空文字。

PICKUP_COND=
# 〜 中略 〜
while getopts cf:hm:tuV OPT
do
  case $OPT in
    # 〜 中略 〜
    m ) PICKUP_COND=$OPTARG  ;;
    # 〜 中略 〜
  esac
done

列項目名から列位置への置き換え

-mオプションの引数の列名からawkの列位置への置き換えと、===への置き換えは以下。

PICKUP_COND=`echo $PICKUP_COND | $NAME_TO_POS \
  | sed 's/\([^!~><=]\)=\([^=]\)/\1==\2/g'`

$NAME_TO_POSには、出力する列項目の指定用に作った以下の関数が入ってる。

item_name_to_pos(){
  sed -e 's/id/$1/g'    \
    -e 's/date/$2/g'    \
    -e 's/area1/$3/g'   \
    -e 's/area2/$4/g'   \
    -e 's/area3/$5/g'   \
    -e 's/age/$6/g'     \
    -e 's/sex/$7/g'     \
    -e 's/job/$8/g'     \
    -e 's/note/$9/g'    \
    -e 's/sts/$10/g'
}

単純に単語を置き換えているだけ。
つまり、列名以外の条件式にパラメータ名(idとかdateとか)が出現した場合も置き換えちゃう。
(-m 'job~"job"'とした場合、$8~"$8"に置き換える)

なんてこった。
とはいえそんな条件を使うことがパッと思い浮かばないので、問題になったら対策しましょ。

ちなみに===への置き換えも同じ課題を抱える。
なんてこった。

出力

出力するコードのケースは前回までと同じ。
(ただ、本記事を執筆時にヘッダの出力処理を追加した)

$PICKUP_CONDに引数で渡した条件式が入り、あとはawkがよろしくやってくれる。
条件未指定の場合は、$PICKUP_CONDは空なので、全行出力となる。

else
  # output pickup headr
  head -n 1 "$DB_FILE"  \
    | awk -F"," '
      BEGIN{ OFS="," }
      {
        print '"${OUTPUT_ITEMS}"'
      }'

  # output pickup recodes
  cat "$DB_FILE"    \
    | sed '1d'      \
    | awk -F"," '
      BEGIN{ OFS="," }
      '"$PICKUP_COND"'{
        print '"${OUTPUT_ITEMS}"'
      }'
fi

awkすごい。

カウント

行数をカウントして出力しているのは以下。
-cオプションが指定された場合に通るケース。
sedで1行消しているのはヘッダ行。

elif [ -n "$CNT_OF_RECODE" ]; then
  # output count of pickup recodes
  cat "$DB_FILE"    \
    | sed '1d'      \
    | awk -F"," '
      BEGIN{ count=0 }
      '"$PICKUP_COND"'{
        count++
      }
      END{ print count }'
else

さいごに

行の抽出もawkのおかげでサクサク。

これを正規表現でやろうとすると超絶ウルトラスーパーツラい。

ありがとうawk
ありがとうawkを作った皆さま。

次回は、列項目内の内訳集計処理。

確認環境

PC Thinkpad X1 Carbon 2nd Gen
OS FreeBSD 12.1-RELEASE-p4

参考