対象のCSVについて
対象のデータは
https://covid19.mhlw.go.jp/public/opendata/newly_confirmed_cases_daily.csv
カレントディレクトリにダウンロードします。
とりあえずファイルを読み込む
(with-open-file (in "./newly_confirmed_cases_daily.csv")
(loop for line = (read-line in nil nil)
while line
collect line))
with-open-file でストリームを作り、read-line で一行ずつ読み取るループを回します。collect でリストにして返します。
確認のための関数をつくる
データの件数が多いため、head と tail で簡単に確認できるようにしたい。それぞれの関数を定義します。
(defun head (list n)
(butlast list (- (length list) n)))
(defun tail (list n)
(last list n))
butlast は便利です。
先頭と末尾の5行を確認する
head と tail を append して、中身を見てみます。
(let ((data (with-open-file (in "./newly_confirmed_cases_daily.csv")
(loop for line = (read-line in nil nil)
while line
collect line))))
(append (head data 5)
(tail data 5)))
#|
("Date,Prefecture,Newly confirmed cases
" "2020/1/26,ALL,1
"
"2020/1/26,Hokkaido,0
" "2020/1/26,Aomori,0
" "2020/1/26,Iwate,0
"
"2021/10/3,Kumamoto,6
" "2021/10/3,Oita,10
" "2021/10/3,Miyazaki,0
"
"2021/10/3,Kagoshima,0
" "2021/10/3,Okinawa,29
")
|#
ぱっと見た感じ、文字列がCR (リターン)で終わっていること、カンマが含まれていることが問題に思われます。
改行コードがCRLFのようです
string-right-trim でCRを除いてしまいます。
(let ((data (with-open-file (in "./newly_confirmed_cases_daily.csv")
(loop for line = (read-line in nil nil)
while line
collect (string-right-trim '(#\return) line)))))
(append (head data 5)
(tail data 5)))
;; ("Date,Prefecture,Newly confirmed cases" "2020/1/26,ALL,1"
;; "2020/1/26,Hokkaido,0" "2020/1/26,Aomori,0" "2020/1/26,Iwate,0"
;; "2021/10/3,Kumamoto,6" "2021/10/3,Oita,10" "2021/10/3,Miyazaki,0"
;; "2021/10/3,Kagoshima,0" "2021/10/3,Okinawa,29")
カンマをスペースに置き換えたい
後で文字列をS式に変換しますが、カンマが含まれていると準クォート (`)のエスケープのためのカンマと認識されエラーが出ます。なので予めスペースに置き換えてしまいましょう。
(let ((data (with-open-file (in "./newly_confirmed_cases_daily.csv")
(loop for line = (read-line in nil nil)
while line
collect (substitute #\space
#\,
(string-right-trim '(#\return)
line))))))
(append (head data 5)
(tail data 5)))
;; ("Date Prefecture Newly confirmed cases" "2020/1/26 ALL 1"
;; "2020/1/26 Hokkaido 0" "2020/1/26 Aomori 0" "2020/1/26 Iwate 0"
;; "2021/10/3 Kumamoto 6" "2021/10/3 Oita 10" "2021/10/3 Miyazaki 0"
;; "2021/10/3 Kagoshima 0" "2021/10/3 Okinawa 29")
S式っぽくなってきました。
先頭のヘッダ行をスキップする
ヘッダ行のスキップは、簡単にリストの cdr を取ることで除外します。
(let ((data (cdr (with-open-file (in "./newly_confirmed_cases_daily.csv")
(loop for line = (read-line in nil nil)
while line
collect (substitute #\space
#\,
(string-right-trim '(#\return)
line)))))))
(append (head data 5)
(tail data 5)))
;; ("2020/1/26 ALL 1" "2020/1/26 Hokkaido 0" "2020/1/26 Aomori 0"
;; "2020/1/26 Iwate 0" "2020/1/26 Miyagi 0" "2021/10/3 Kumamoto 6"
;; "2021/10/3 Oita 10" "2021/10/3 Miyazaki 0" "2021/10/3 Kagoshima 0"
;; "2021/10/3 Okinawa 29")
テキストをS式に変換したい
行を concatenate で ()ではさみ、read-from-string でS式にする。
(let ((data (cdr
(with-open-file (in "./newly_confirmed_cases_daily.csv")
(loop for line = (read-line in nil nil)
while line
collect (read-from-string
(concatenate 'string "("
(substitute #\space #\,
(string-right-trim
'(#\return)
line)) ")")))))))
(append (head data 5)
(tail data 5)))
;; ((|2020/1/26| ALL 1) (|2020/1/26| HOKKAIDO 0) (|2020/1/26| AOMORI 0)
;; (|2020/1/26| IWATE 0) (|2020/1/26| MIYAGI 0) (|2021/10/3| KUMAMOTO 6)
;; (|2021/10/3| OITA 10) (|2021/10/3| MIYAZAKI 0) (|2021/10/3| KAGOSHIMA 0)
;; (|2021/10/3| OKINAWA 29))
まさにS式になりました。
再利用しやすくするため関数にする
たびたび使うので get-data として関数にします。環境を汚さないためにも let を使って局所変数にバインドしています。
(defun get-data ()
(cdr (with-open-file (in "./newly_confirmed_cases_daily.csv")
(loop for line = (read-line in nil nil)
while line
collect (read-from-string
(concatenate 'string "("
(substitute #\space #\, (string-right-trim
'(#\return)
line)) ")"))))))
(let ((data (get-data)))
(append (head data 5)
(tail data 5)))
;; ((|2020/1/26| ALL 1) (|2020/1/26| HOKKAIDO 0) (|2020/1/26| AOMORI 0)
;; (|2020/1/26| IWATE 0) (|2020/1/26| MIYAGI 0) (|2021/10/3| KUMAMOTO 6)
;; (|2021/10/3| OITA 10) (|2021/10/3| MIYAZAKI 0) (|2021/10/3| KAGOSHIMA 0)
;; (|2021/10/3| OKINAWA 29))
愛知県のデータを抽出する
remove-if-not で 'aichi 以外を除去する。sort で (car data) を昇順にする。let が let* に変更されている点に注意。
(let* ((data (get-data))
(data-aichi (sort (remove-if-not (lambda (line)
(eq (cadr line) 'aichi))
data)
#'string<
:key #'car)))
(append (head data-aichi 5)
(tail data-aichi 5)))
;; ((|2020/1/26| AICHI 1) (|2020/1/27| AICHI 0) (|2020/1/28| AICHI 1)
;; (|2020/1/29| AICHI 0) (|2020/1/30| AICHI 0) (|2021/9/5| AICHI 1376)
;; (|2021/9/6| AICHI 1190) (|2021/9/7| AICHI 1217) (|2021/9/8| AICHI 1289)
;; (|2021/9/9| AICHI 1169))
日付のリストを取得する
data の car の リスト から、remove-duplicates で重複を除去します。
(let* ((data (get-data))
(data-dates (remove-duplicates (mapcar (lambda (line)
(car line))
data))))
(append (head data-dates 5)
(tail data-dates 5)))
;; (|2020/1/26| |2020/1/27| |2020/1/28| |2020/1/29| |2020/1/30| |2021/9/29|
;; |2021/9/30| |2021/10/1| |2021/10/2| |2021/10/3|)
年月日のシンボルから、年月のシンボルを得る
月別に集計したいと思ったとき、年月のシンボルがあれば抽出が簡単になる。そのため、年月日シンボルから年月シンボルを返す関数を定義する。正規表現で抽出してしまいます。quicklisp からの :cl-ppcre を使っています。
(ql:quickload :cl-ppcre)
(defun convert-date-to-year-month (date)
(read-from-string
(concatenate 'string
"|"
(cl-ppcre:scan-to-strings "^\\d+/\\d+"
(symbol-name date))
"|")))
(convert-date-to-year-month '|2021/10/05|)
;; |2021/10|
;; 9
年月のリストを取得する
convert-date-to-year-month を使ってみます。存在する年月日を、年月にして重複除去します。つまり年月をユニークに取得します。
(let* ((data (cdr (get-data)))
(data-dates (remove-duplicates (mapcar (lambda (line)
(car line))
data))))
(remove-duplicates
(mapcar (lambda (date) (convert-date-to-year-month date))
data-dates)))
;; (|2020/1| |2020/2| |2020/3| |2020/4| |2020/5| |2020/6| |2020/7| |2020/8|
;; |2020/9| |2020/10| |2020/11| |2020/12| |2021/1| |2021/2| |2021/3| |2021/4|
;; |2021/5| |2021/6| |2021/7| |2021/8| |2021/9| |2021/10|)
年月の情報を付与する
data を mapcarするなかで append すれば 列を増やすことができる。
(let* ((data (cdr (get-data)))
(data-year-month
(mapcar (lambda (line)
(append line
`(,(convert-date-to-year-month (car line)))))
data)))
(append (head data-year-month 5)
(tail data-year-month 5)))
;; ((|2020/1/26| HOKKAIDO 0 |2020/1|) (|2020/1/26| AOMORI 0 |2020/1|)
;; (|2020/1/26| IWATE 0 |2020/1|) (|2020/1/26| MIYAGI 0 |2020/1|)
;; (|2020/1/26| AKITA 0 |2020/1|) (|2021/10/3| KUMAMOTO 6 |2021/10|)
;; (|2021/10/3| OITA 10 |2021/10|) (|2021/10/3| MIYAZAKI 0 |2021/10|)
;; (|2021/10/3| KAGOSHIMA 0 |2021/10|) (|2021/10/3| OKINAWA 29 |2021/10|))
愛知の2021年9月
data-year-month に '|2021/9| による remove-if-not 、さらに、'aichi による remove-if-not を適用ま
す。
(let* ((data (cdr (get-data)))
(data-year-month
(mapcar (lambda (line)
(append line
`(,(convert-date-to-year-month (car line)))))
data))
(data-ym-aichi
(remove-if-not (lambda (line)
(eq (second line) 'aichi))
(remove-if-not (lambda (line)
(eq (fourth line) '|2021/9|))
data-year-month))))
data-ym-aichi)
;; ((|2021/9/1| AICHI 1876 |2021/9|) (|2021/9/2| AICHI 1718 |2021/9|)
;; (|2021/9/3| AICHI 1720 |2021/9|) (|2021/9/4| AICHI 1776 |2021/9|)
;; (|2021/9/5| AICHI 1376 |2021/9|) (|2021/9/6| AICHI 1190 |2021/9|)
;; (|2021/9/7| AICHI 1217 |2021/9|) (|2021/9/8| AICHI 1289 |2021/9|)
;; (|2021/9/9| AICHI 1169 |2021/9|) (|2021/9/10| AICHI 1031 |2021/9|)
;; (|2021/9/11| AICHI 970 |2021/9|) (|2021/9/12| AICHI 556 |2021/9|)
;; (|2021/9/13| AICHI 211 |2021/9|) (|2021/9/14| AICHI 655 |2021/9|)
;; (|2021/9/15| AICHI 636 |2021/9|) (|2021/9/16| AICHI 562 |2021/9|)
;; (|2021/9/17| AICHI 390 |2021/9|) (|2021/9/18| AICHI 420 |2021/9|)
;; (|2021/9/19| AICHI 327 |2021/9|) (|2021/9/20| AICHI 183 |2021/9|)
;; (|2021/9/21| AICHI 151 |2021/9|) (|2021/9/22| AICHI 270 |2021/9|)
;; (|2021/9/23| AICHI 359 |2021/9|) (|2021/9/24| AICHI 173 |2021/9|)
;; (|2021/9/25| AICHI 213 |2021/9|) (|2021/9/26| AICHI 166 |2021/9|)
;; (|2021/9/27| AICHI 73 |2021/9|) (|2021/9/28| AICHI 139 |2021/9|)
;; (|2021/9/29| AICHI 155 |2021/9|) (|2021/9/30| AICHI 132 |2021/9|))
愛知の2021年9月の合計
reduce で date-ym-aichi の third を合計します。
(let* ((data (cdr (get-data)))
(data-year-month
(mapcar (lambda (line)
(append line
`(,(convert-date-to-year-month (car line)))))
data))
(data-ym-aichi
(remove-if-not (lambda (line)
(eq (second line) 'aichi))
(remove-if-not (lambda (line)
(eq (fourth line) '|2021/9|))
data-year-month))))
(reduce #'+ (mapcar (lambda (line)
(third line))
data-ym-aichi)))
;; 21103
都道府県のリストを取得する
date を mapcar して second のリストを remove-duplicates します。
(let* ((data (cdr (get-data)))
(data-prefs (remove-duplicates (mapcar (lambda (line)
(second line))
data))))
data-prefs)
;; (ALL HOKKAIDO AOMORI IWATE MIYAGI AKITA YAMAGATA FUKUSHIMA IBARAKI TOCHIGI
;; GUNMA SAITAMA CHIBA TOKYO KANAGAWA NIIGATA TOYAMA ISHIKAWA FUKUI YAMANASHI
;; NAGANO GIFU SHIZUOKA AICHI MIE SHIGA KYOTO OSAKA HYOGO NARA WAKAYAMA TOTTORI
;; SHIMANE OKAYAMA HIROSHIMA YAMAGUCHI TOKUSHIMA KAGAWA EHIME KOCHI FUKUOKA SAGA
;; NAGASAKI KUMAMOTO OITA MIYAZAKI KAGOSHIMA OKINAWA)
ALLは要らない
さらに 'all を remove-if することで all を除外します。
(let* ((data (get-data))
(data-prefs (remove-if (lambda (pref) (eq pref 'all))
(remove-duplicates (mapcar (lambda (line)
(second line))
data)))))
data-prefs)
;; (HOKKAIDO AOMORI IWATE MIYAGI AKITA YAMAGATA FUKUSHIMA IBARAKI TOCHIGI GUNMA
;; SAITAMA CHIBA TOKYO KANAGAWA NIIGATA TOYAMA ISHIKAWA FUKUI YAMANASHI NAGANO
;; GIFU SHIZUOKA AICHI MIE SHIGA KYOTO OSAKA HYOGO NARA WAKAYAMA TOTTORI SHIMANE
;; OKAYAMA HIROSHIMA YAMAGUCHI TOKUSHIMA KAGAWA EHIME KOCHI FUKUOKA SAGA NAGASAKI
;; KUMAMOTO OITA MIYAZAKI KAGOSHIMA OKINAWA)
##都道府県の件数を確認する
念の為、件数を確認します。
(let* ((data (get-data))
(data-prefs (remove-if (lambda (pref) (eq pref 'all))
(remove-duplicates (mapcar (lambda (line)
(second line))
data)))))
(length data-prefs))
;; 47
都道府県ごとの合計
data-pref を mapcar します。その中で、それぞれの都道府県の合計を求めます。
(let* ((data (get-data))
(data-prefs (remove-if (lambda (pref) (eq pref 'all))
(remove-duplicates (mapcar (lambda (line)
(second line))
data)))))
(mapcar (lambda (pref)
`(,pref
,(reduce #'+
(mapcar (lambda (line-pref)
(third line-pref))
(remove-if-not (lambda (line)
(eq (second line) pref))
data)))))
data-prefs))
;; ((HOKKAIDO 60289) (AOMORI 5711) (IWATE 3480) (MIYAGI 16264) (AKITA 1881)
;; (YAMAGATA 3495) (FUKUSHIMA 9456) (IBARAKI 24128) (TOCHIGI 15213) (GUNMA 16681)
;; (SAITAMA 114720) (CHIBA 99417) (TOKYO 375582) (KANAGAWA 167393) (NIIGATA 7934)
;; (TOYAMA 4807) (ISHIKAWA 7959) (FUKUI 3061) (YAMANASHI 5163) (NAGANO 8779)
;; (GIFU 18517) (SHIZUOKA 26296) (AICHI 105123) (MIE 14624) (SHIGA 12330)
;; (KYOTO 35525) (OSAKA 200038) (HYOGO 77162) (NARA 15383) (WAKAYAMA 5280)
;; (TOTTORI 1620) (SHIMANE 1632) (OKAYAMA 15118) (HIROSHIMA 21780)
;; (YAMAGUCHI 5604) (TOKUSHIMA 3262) (KAGAWA 4683) (EHIME 5195) (KOCHI 4121)
;; (FUKUOKA 73942) (SAGA 5767) (NAGASAKI 5994) (KUMAMOTO 14332) (OITA 8108)
;; (MIYAZAKI 6118) (KAGOSHIMA 9042) (OKINAWA 49780))
まとめ
いくつか集計を試してみました。切がないのでこれぐらいにしておきます。複雑に見えますが。。。、残念ながら実際に複雑です。一年後の自分はこれをメンテナンスできるでしょうか?
まあ慣れでしょう。そして今回試した限り、コツがあると感じます。S式の構造を正確に保ち続けることのような気がします。pareditにかなり助けられました。
以上、ダラダラ書きでした。最後までお付き合いありがとうございました。