LoginSignup
4
1

More than 1 year has passed since last update.

common lispでcsvファイルのデータ抽出・集計やってみた

Last updated at Posted at 2021-10-05

対象の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にかなり助けられました。

以上、ダラダラ書きでした。最後までお付き合いありがとうございました。

4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1