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

## 対象のCSVについて
対象のデータは
https://covid19.mhlw.go.jp/public/opendata/newly_confirmed_cases_daily.csv

カレントディレクトリにダウンロードします。

## とりあえずファイルを読み込む
```common-lisp
(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 で簡単に確認できるようにしたい。それぞれの関数を定義します。

```common-lisp
(defun head (list n)
  (butlast list (- (length list) n)))

(defun tail (list n)
  (last list n))
```

butlast は便利です。

## 先頭と末尾の5行を確認する
head と tail を append して、中身を見てみます。

```common-lisp
(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を除いてしまいます。

```common-lisp
(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式に変換しますが、カンマが含まれていると準クォート (`)のエスケープのためのカンマと認識されエラーが出ます。なので予めスペースに置き換えてしまいましょう。

```common-lisp
(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 を取ることで除外します。

```common-lisp
(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式にする。

```common-lisp
(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 を使って局所変数にバインドしています。

```common-lisp
(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* に変更されている点に注意。

```common-lisp
(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 で重複を除去します。

```common-lisp
(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 を使っています。

```common-lisp
(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 を使ってみます。存在する年月日を、年月にして重複除去します。つまり年月をユニークに取得します。

```common-lisp
(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 すれば 列を増やすことができる。

```common-lisp
(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 を適用ま
す。

```common-lisp
(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 を合計します。

```common-lisp
(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 します。

```common-lisp
(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 を除外します。

```common-lisp
(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)
```

##都道府県の件数を確認する
念の為、件数を確認します。

```common-lisp
(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 します。その中で、それぞれの都道府県の合計を求めます。

```common-lisp
(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にかなり助けられました。

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