【シェルと遊ぼう】日本のコロナ感染者情報取得スクリプト その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

参考