演習8: File I/Oの利用とこれまでの復習(CSV形式データの読み込みと集計処理)
- 演習8について
- サブ演習毎にdriver/observerを交代しよう。席の左右どちらから始めても構いません。8.1を右の人がdriverやったら、8.2は左の人がやろう。
- 演習8: CSV形式データの読み込みと集計処理を実装してみよう。
- open(), read(), write() を使ってみよう。できればwith構文を使おう。
- 参考: 教科書 Chapter 4.6, 授業8回目
- リスト操作、文字列操作に慣れよう。
- 参考: 教科書 Chapter 5.2, 5.4, 授業10回目
- 変数名は命名規則を参考に付けよう。(説明時に変数x等と名づけているのは、説明上の都合です。実装上は適切な変数名で処理しよう)
- 概要
- 背景
- CSV(もしくはCSVファイル)とは、複数のデータをカンマ区切りでテキストファイルの総称であり、通常は .csv という拡張子を付けることが多い。中身はテキストファイルであるため、任意のエディタで参照・編集可能である。その扱いやすさから多数のライブラリが開発されているが、ここでは自前で処理しよう。
- 今回処理するCSVファイルとして
score.csvを用意した。演習8.1以下ではこのファイルをダウンロードして利用すること。
- このファイルは「学生毎にレポートの採点結果をカンマ区切りで列挙」している。
- 1行目はコメント行(実際には無関係のデータ)。
- 2行目以降は「学生のアカウント名,レポート1の点数,レポート2の点数,レポート3の点数」の形式で採点結果が列挙されている。
- score.csv全体としては、コメント行と、5人分の採点結果が列挙されていることになる。
- 本演習では、score.csvを読み込み、学生毎に点数を集計するコードを書いてみよう。
- 最終目標は、学生毎に「アカウント名,最小点,平均点,最高得点」という形式でファイルに出力することである。(出力例: result.csv)
- 補足
- read_csvfile() は、引数としてファイル名を受け取るものとする。
- read_csvfile() は、1行を1つのstr型オブジェクトとし、それらを要素に持つリストを返すものとする。(下記実行例参照)
- ファイルを読み込み終えたら、ファイルハンドラを close すること。(もしくはwith構文で自動closeさせること)
- 実行例
# read_csvfile() の実行例
# (以下はインタプリタの例だが、実行結果を確認できるならスクリプトファイルでもOK)
>>> 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() の実行例
>>> 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()は、前処理を施したデータ(リスト型)を返すものとする。(下記実行例参照)
前処理
- (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型へ型変換しよう。
- 実装上の補足
実行例
# 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)。実装しやすいように関数設計してみよう。
# 実行例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()の実行例
>>> data = read_csvfile(filename)
>>> target = preprocess(data)
>>> result = analysis(target)
>>> result
[['e175701', 89, 91.0, 95], ['e175702', 79, 83.0, 89], ['e175703', 60, 63.333333333333336, 70], ['e175704', 95, 101.66666666666667, 110], ['e175705', 0, 0.0, 0]]
- 上記実行例では、analysis()という一つの関数で複数のタスクをこなしているが、必要に応じてより小さな関数群を作っても良い。実装しやすいように関数設計してみよう。
- 補足
- output()は、引数として(1)分析結果(リスト型)、(2)保存するファイル名、の2つの引数を受け取るものとする。
- CSV出力の形式は下記実行例を参照。
- 実行例
- ターミナル上の出力なし。
- out_filenameで指定したファイル名に出力される内容: result.csv
>>> data = read_csvfile(filename)
>>> target = preprocess(data)
>>> result = analysis(target)
>>> out_filename = 'result.csv'
>>> output(result, out_filename)
>>>
# ターミナル上の出力なし。
- 補足
- 演習8.5までのコードを week11.py という名前で保存し、作成した全ての関数について簡易説明(ドキュメント)をdocstringにて書け。
- (a)関数についての1行概要、(b)引数、(c)戻り値について記述すること。
- 例えばレポート2: tic_tac.toe.pyの init_board() を参考に取ると、各々次のように記述している。
- (a) Return the empty board.(空のボードを返す)
- (b) None (引数はない)
- (c) list: the empty board with 9 items. (リスト型: 9つの要素を持つ空ボード)
- 任意の関数2つについて、ユニットテストを記述せよ。この際、関数すべての機能についてテストする必要はなく、一部の動作が確認できれば良い。どの部分をテストするかも自由である。
- 参考: レポート2: tic_tac.toe.py
(余裕のあるペア向け) 演習8.x: 辞書型を使ってみよう。
- 補足
['e175701', 95, 89, 89]
のように、「集計すべきデータがアカウント名の次から列挙されている」というのはデータ構造として扱いにくい。辞書型(dict型)を利用して{'e175701':[95, 89, 89]}
のように、アカウント名をキーと指定すると、そのスコア一覧をリストとして参照できるようにデータを用意できると便利そうである。どう整形したら良いだろうか?