33. 演習7: CSV形式データを読み込み、集計処理を実装してみよう(File I/Oの利用とこれまでの復習)。

  • 今後の演習では、指定がない限りはインタプリタ・ファイル編集・IDEいずれの方法で開発するかは好きに選んでもらって構いません。同様に、関数を作れという指示がない場合でも、自由に作成OKです。

  • なお、記録してして残すために「各々の演習を終えた時点でのコードを、スクリプトファイルとして残す」ようにしておくと復習の際にも便利でしょう。


33.1. 達成目標

  • open(), read(), write() を使ってみよう。できればwith構文を使おう。

    • 参考: 教科書 Chapter 4.6, 授業8回目

  • リスト操作、文字列操作に慣れよう。

    • 参考: 教科書 Chapter 5.2, 5.4, 授業7回目

  • 変数名・関数名は命名規則を参考に付けよう。(説明時に変数x等と名づけているのは、説明上の都合です。実装上は適切な変数名で処理しよう)


33.2. 演習7: CSV形式データの読み込みと集計処理を実装してみよう。

  • 概要

    • 背景

      • CSV(もしくはCSVファイル)とは、複数のデータをカンマ区切りでテキストファイルの総称である。一般的には、ファイル名に .csv という拡張子を付けることが多い。中身はテキストファイルであるため、任意のエディタで参照・編集可能である。その扱いやすさから多数のライブラリが開発されているが、ここでは自前で処理しよう。

    • 今回処理するCSVファイルとして score.csvを用意した。演習7.1以下ではこのファイルをダウンロードして利用すること。

      • このファイルは「学生毎にレポートの採点結果をカンマ区切りで列挙」している。

      • 1行目はコメント行(実際には無関係のデータ)。

      • 2行目以降は「学生のアカウント名,レポート1の点数,レポート2の点数,レポート3の点数」の形式で採点結果が列挙されている。

      • score.csv全体としては、コメント行と、5人分の採点結果が列挙されていることになる。

    • 本演習では、score.csvを読み込み、学生毎に点数を集計するコードを書いてみよう。

      • 最終目標は、学生毎に「アカウント名,最小点,平均点,最高得点」という形式でファイルに出力することである。(出力例: result.csv


33.2.1. CSVファイルを読み込みモードで開き、中身を返す関数read_csvfile()を作成せよ。

  • 補足

    • read_csvfile() は、引数としてファイル名を受け取るものとする。

    • read_csvfile() は、1行を1つのstr型オブジェクトとし、それらを要素に持つリストを返すものとする。(下記実行例参照)

    • ファイルを読み込み終えたら、ファイルハンドラを close すること。(もしくはwith構文で自動closeさせること)

    • 実行例

>>> # 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']

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

  • 修正版 read_csvfile() の実行例

    • 違いは改行コードの有無のみ。

>>> # 修正版 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']

33.2.3. 読み込んだデータを処理しやすい形にいくつかの前処理をする関数 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型へ型変換しよう。

    • 実装上の補足

  • 実行例

>>> # 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]]

33.2.4. 学生個々人の最小・平均・最大点数を算出する関数 analysis() を作成せよ

  • 補足

    • analysis()は、引数として前処理を終えたデータ(リスト型)を受け取るものとする。

    • analysis()は、最小・平均・最大得点の算出結果を保持したリストを返すものとする。(下記実行例参照)

    • 平均値は、小数点第2位で四捨五入するものとする。

>>> # 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()という一つの関数で複数のタスクをこなしているが、必要に応じてより小さな関数群を作っても良い。実装しやすいように関数設計してみよう。


33.2.5. 分析結果をファイルにCSV形式で出力する関数 output() を作成せよ

  • 補足

    • 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)
>>>
# ターミナル上の出力なし。

33.2.6. 型ヒント、ドキュメント、ユニットテストの作成

  • 補足

    • 演習7.5までのコードを ex7.py という名前で保存し、作成した全ての関数について型ヒント、docstringドキュメントを書け。

      • ドキュメントは (a)関数についての1行概要、(b)引数、(c)戻り値について記述すること。

    • 任意の関数2つについて、ユニットテスト(doctest)を記述せよ。この際、関数すべての機能についてテストする必要はなく、一部の動作が確認できれば良い。どの部分をテストするかも自由である。


33.2.7. (余裕のあるペア向け) 辞書型を使ってみよう。

  • 補足

    • ['e175701', 95, 89, 89]のように、「集計すべきデータがアカウント名の次から列挙されている」というのはデータ構造として扱いにくい。辞書型(dict型)を利用して{'e175701':[95, 89, 89]}のように、アカウント名をキーと指定すると、そのスコア一覧をリストとして参照できるようにデータを用意できると便利そうである。どう整形したら良いだろうか?

    • 辞書を用意できたら、その辞書を用いて演習7.5を実装し直してみよう。