# 演習7: CSV形式データを読み込み、集計処理を実装してみよう（File I/Oの利用とこれまでの復習）。
- 今後の演習では、<span style="color:red">指定がない限りはインタプリタ・ファイル編集・IDEいずれの方法で開発するかは好きに選んでもらって構いません</span>。同様に、<span style="color:red">関数を作れという指示がない場合でも、自由に作成OK</span>です。
- なお、記録してして残すために「各々の演習を終えた時点でのコードを、スクリプトファイルとして残す」ようにしておくと復習の際にも便利でしょう。

---
## 達成目標
- open(), read(), write() を使ってみよう。できればwith構文を使おう。
  - 参考: 教科書 Chapter 4.6, 授業8回目
- リスト操作、文字列操作に慣れよう。
  - 参考: 教科書 Chapter 5.2, 5.4, 授業7回目
- 変数名・関数名は[命名規則](https://google.github.io/styleguide/pyguide.html#Naming)を参考に付けよう。（説明時に変数x等と名づけているのは、説明上の都合です。実装上は適切な変数名で処理しよう）

---
## 演習7: CSV形式データの読み込みと集計処理を実装してみよう。
- 概要
  - 背景
    - CSV（もしくはCSVファイル）とは、複数のデータをカンマ区切りでテキストファイルの総称である。一般的には、ファイル名に .csv という拡張子を付けることが多い。中身はテキストファイルであるため、任意のエディタで参照・編集可能である。その扱いやすさから多数のライブラリが開発されているが、ここでは自前で処理しよう。
      - 参考: [CSV](https://ja.wikipedia.org/wiki/Comma-Separated_Values)
  - 今回処理するCSVファイルとして
  [score.csv](https://raw.githubusercontent.com/naltoma/python_intro2021/master/assets/samples/score.csv)を用意した。演習7.1以下ではこのファイルをダウンロードして利用すること。
    - このファイルは「学生毎にレポートの採点結果をカンマ区切りで列挙」している。
    - 1行目はコメント行（実際には無関係のデータ）。
    - 2行目以降は「学生のアカウント名,レポート1の点数,レポート2の点数,レポート3の点数」の形式で採点結果が列挙されている。
    - score.csv全体としては、コメント行と、5人分の採点結果が列挙されていることになる。
  - 本演習では、score.csvを読み込み、学生毎に点数を集計するコードを書いてみよう。
    - 最終目標は、学生毎に「アカウント名,最小点,平均点,最高得点」という形式でファイルに出力することである。（出力例: [result.csv](https://raw.githubusercontent.com/naltoma/python_intro2021/master/assets/samples/result.csv)）

---
### CSVファイルを読み込みモードで開き、中身を返す関数read_csvfile()を作成せよ。
- 補足
  - read_csvfile() は、引数としてファイル名を受け取るものとする。
  - read_csvfile() は、1行を1つのstr型オブジェクトとし、それらを要素に持つリストを返すものとする。（下記実行例参照）
  - ファイルを読み込み終えたら、ファイルハンドラを close すること。（もしくはwith構文で自動closeさせること）
  - 実行例

```python
>>> # read_csvfile() の実行例
>>> filename = 'score.csv'
>>> data = read_csvfile(filename)
>>> print(len(data))
6
>>> print(data)
['account,report1_score,report2_score,report3_score\n', 'e175701,95,89,89\n', 'e175702,81,89,79\n', 'e175703,70,60,60\n', 'e175704,110,95,100\n', 'e175705,0,0,0\n']
```

---
### read_csvfile()で読み込んだデータには、行末の改行コード``\n``が含まれている。read_csvfile()を修正して、行末コードを削除した要素となるようにせよ。
- 修正版 read_csvfile() の実行例
  - 違いは改行コードの有無のみ。

```python
>>> # 修正版 read_csvfile() の実行例
>>> data = read_csvfile(filename)
>>> print(data)
['account,report1_score,report2_score,report3_score', 'e175701,95,89,89', 'e175702,81,89,79', 'e175703,70,60,60', 'e175704,110,95,100', 'e175705,0,0,0']
```

---
### 読み込んだデータを処理しやすい形にいくつかの前処理をする関数 preprocess() を作成せよ
- 補足
  - preprocess()は、引数として読み込んだデータ（リスト型）を受け取るものとする。
  - preprocess()は、前処理を施したデータ（リスト型）を返すものとする。（下記実行例参照）
- 前処理
  - (1) 読み込んだデータの1つ目の要素``data[0]``はコメント行のため、集計作業は不要である。これを削除しよう。
  - (2) 読み込んだデータの2つ目以降の要素（``data[1]〜data[-1]``）は、各々が「アカウント名,点数1,点数2,点数3」という書式で1つのstr型オブジェクトになっている。
    - (2-1) このままでは個別の要素にアクセスすることができない。CSV形式、つまりカンマでデータが区切られているため、カンマを区切り文字として分割しよう。
    - (2-2) str型オブジェクトを分割した時点では、全ての要素がstr型である。例えば``'e175701,95,89,89'``をカンマで分割しても``['e175701','95','89','89']``のように点数1〜3はint型ではなくstr型のままである。集計しやすくするために、点数1〜3についてはint型へ型変換しよう。
  - 実装上の補足

- 実行例

```python
>>> # preprocess()の実行例
>>> data = read_csvfile(filename)
>>> target = preprocess(data)
>>> len(target)
5
>>> print(target)
[['e175701', 95, 89, 89], ['e175702', 81, 89, 79], ['e175703', 70, 60, 60], ['e175704', 110, 95, 100], ['e175705', 0, 0, 0]]
```

- 上記実行例では、preprocess()という一つの関数で上記(1),(2-1),(2-2)合計3つのタスクをこなしているが、必要に応じてより小さな関数群を作っても良い（e.g., 下記実行例2）。実装しやすいように関数設計してみよう。

```python
>>> # 実行例2
>>> # preprocess()相当の機能を複数の関数に分割して実装した例。
>>> # 実行例1, 実行例2のように、最終出力が同一であれば関数設計は自由で構わない。
>>> data = read_csvfile(filename)
>>> data2 = remove_comment_line(data)
>>> print(data2)
['e175701,95,89,89', 'e175702,81,89,79', 'e175703,70,60,60', 'e175704,110,95,100', 'e175705,0,0,0']
>>> data3 = split_data(data2)
>>> print(data3)
[['e175701', '95', '89', '89'], ['e175702', '81', '89' , '79'], ['e175703', '70', '60', '60'], ['e175704', '110', '95', '100'], ['e175705', '0', '0', '0']]
>>> target = scores_to_int(data3)
>>> print(target)
[['e175701', 95, 89, 89], ['e175702', 81, 89 , 79], ['e175703', 70, 60, 60], ['e175704', 110, 95, 100], ['e175705', 0, 0, 0]]
```

---
### 学生個々人の最小・平均・最大点数を算出する関数 analysis() を作成せよ
- 補足
  - analysis()は、引数として前処理を終えたデータ（リスト型）を受け取るものとする。
  - analysis()は、最小・平均・最大得点の算出結果を保持したリストを返すものとする。（下記実行例参照）
  - 平均値は、小数点第2位で四捨五入するものとする。

```python
>>> # analysis()の実行例
>>> data = read_csvfile(filename)
>>> target = preprocess(data)
>>> result = analysis(target)
>>> result
[['e175701', 89, 91.0, 95], ['e175702', 79, 83.0, 89], ['e175703', 60, 63.3, 70], ['e175704', 95, 101.7, 110], ['e175705', 0, 0.0, 0]]
```

- 上記実行例では、analysis()という一つの関数で複数のタスクをこなしているが、必要に応じてより小さな関数群を作っても良い。実装しやすいように関数設計してみよう。

---
### 分析結果をファイルにCSV形式で出力する関数 output() を作成せよ
- 補足
  - output()は、引数として(1)分析結果（リスト型）、(2)保存するファイル名、の2つの引数を受け取るものとする。
  - CSV出力の形式は下記実行例を参照。
- 実行例
  - ターミナル上の出力なし。
  - out_filenameで指定したファイル名に出力される内容: [result.csv](https://ie.u-ryukyu.ac.jp/~tnal/2016/prog1/result.csv)

```python
>>> data = read_csvfile(filename)
>>> target = preprocess(data)
>>> result = analysis(target)
>>> out_filename = 'result.csv'
>>> output(result, out_filename)
>>>
# ターミナル上の出力なし。
```

---
### 型ヒント、ドキュメント、ユニットテストの作成
- 補足
  - 演習7.5までのコードを ex7.py という名前で保存し、作成した全ての関数について型ヒント、docstringドキュメントを書け。
    - ドキュメントは (a)関数についての1行概要、(b)引数、(c)戻り値について記述すること。
  - 任意の関数2つについて、ユニットテスト(doctest)を記述せよ。この際、関数すべての機能についてテストする必要はなく、一部の動作が確認できれば良い。どの部分をテストするかも自由である。

---
### (余裕のあるペア向け) 辞書型を使ってみよう。
- 補足
  - ``['e175701', 95, 89, 89]``のように、「集計すべきデータがアカウント名の次から列挙されている」というのはデータ構造として扱いにくい。辞書型(dict型)を利用して``{'e175701':[95, 89, 89]}``のように、アカウント名をキーと指定すると、そのスコア一覧をリストとして参照できるようにデータを用意できると便利そうである。どう整形したら良いだろうか？
  - 辞書を用意できたら、その辞書を用いて演習7.5を実装し直してみよう。
