[Work/Class/Python3の基礎とデータ処理/python_basic]

CSVの取得とPlot, pandas.DataFrameの取り扱い

Jupyter NotebookとGoogle Colaboratory

これまではローカルに構築したAnacondaのPythonで実行していたが,データ分析だけならクラスごとにソースコードを分離したりなど,高度なプログラミングをする必要がないので,ブラウザ上でコードを編集して,そのまま実行できるJupyter NotebookををAnacondaから実行するのが良い.

ただしその場合でも環境を設定したりなどの必要がある.Google Colaboratoryを使うと,環境設定その他諸々が一切いらない,つまり自分のコンピュータではなくGoogleが用意した環境で実行できるので便利である.

CSVファイルのインターネットからのダウンロードと,CSVを開いてグラフを描画する

htmlファイルの取得でもやったurllib.request.open()という関数でダウンロードし,一度ファイルをテキストファイルとして保存,その後pandasというライブラリを使ってDataFrameという形式で読み込むのが,2019年現在での一般的な方法である.

Google Colab & Jupyter Notebookのコード例

REPLと呼び,処理をある程度まとめて「セル」というブロックに記述して実行して,その結果を見ながら次を実行する,など,逐次的に実行していくのが特徴.

1つ目のセル.一度読み込んだ後に書き出しておかないとpandasではどうやら読めない模様.

import urllib.request
import codecs

# 八王子市の人口データを,インターネット経由で読み込む.
http_response = urllib.request.urlopen('http://www.city.hachioji.tokyo.jp/contents/open/002/p005866_d/fil/nenreibetsu_jinkou_2506.csv')
if http_response.code == 200:
  print('Sucess to get CSV file from Internet')

  # 文字コード(text_encode)を取得する.
  text_encode = http_response.info().get_content_charset() 
  print(text_encode)
  # ここで出てきた文字コードを次に突っ込めばいいのだが,
  # htmlとは違い,CSVファイルには文字コードが入っていないことが多い.
  # なので文字コードはなんとなく指定するしかない.
  
  # 文字コード
  csv_text = http_response.read().decode('SHIFT-JIS')
  # 公的機関とかのCSVファイルは,文字コードが大体SHIFT-JIS
  # まずutf-8を試して,ダメならSHIFT-JISにしてみること

  #確かめるために一度表示
  print(csv_text)
  
  #pandasの機能で読み込むために,一度ファイルとして書き出しておく
  temporary_writing_file = codecs.open('current_analyzing.csv', mode='w', encoding='utf-8')
  temporary_writing_file.write(csv_text)
  temporary_writing_file.close()
  
  # ここまでの処理が成功したら以下の文をプリントする
  print('Sucsess to save downloaded CSV file as utf-8')
  
else:
  # httpで取得することが失敗だったら,失敗したコードを表示
  print('Failed to get CSV file from internet. Error Code is:', http_response.code)

結果は以下のように出力される.ちゃんとCSVファイルの内容がちゃんと取れている.

Sucess to get CSV file from Internet
None
対象年月,年齢,人口_合計,人口_男,人口_女
201306,0,3989,2041,1948
201306,1,4249,2217,2032
201306,2,4494,2318,2176
201306,3,4719,2440,2279
...(続く)

文字コードを表示するはずの2行目が「None」なので,やはりCSVファイルからは文字コードは推定できないようだ.

2つめのセル.

import numpy as np
import pandas as pd

# 前のセルで一度utf-8で書き出しておいたCSVを読み込む
dataframe = pd.read_csv('current_analyzing.csv',  encoding='utf-8')
print(dataframe)

dataframe.head()

pandas.read_csv関数で読み込むと, DataFrameという形式で読み込んでくれる.このDataFrameは2次元の表に「行と列,それぞれにラベルがついている」データ形式であり,さらに複数のデータ型,例えば整数や実数,文字列を,一つの表の中に混在させることができる.(ただしもちろんデータとしてまとまりがなければならないので,行か列のどちらかでデータ型は揃っている必要はある)

DataFrameオブジェクトのhead関数は,わかりやすく頭の部分だけを出力してくれる.

dataframe.head()のGoogle Colaboratoryでの実行結果

3つ目のセル.pandasで読み込むDataFrame形式の真骨頂で「列だけ」を「ラベルを使って」取り出すことができる

locという関数(正確にはオペレータ)を使うが,
loc[:, '列のラベル']のように取り出す.

最初の:は「行は全部」(0~あるだけ)という意味.
loc[0:, '列のラベル']とも書ける.

dataframe.loc[1:3, '人口_男']と書くと,
'人口_男'列の「1行目から3行目」を抽出する,という意味になる.
(行番号は0から始まることに注意)

# それぞれ「列」に分解していく

# 年齢の「列」だけを,列のラベルを使って,1次元配列で取り出していく
age = dataframe.loc[:, '年齢']
population_man = dataframe.loc[:, '人口_男']
population_women = dataframe.loc[:, '人口_女']
population_sum = dataframe.loc[:, '人口_合計']

上記では,列にラベルがある場合を想定したが,もちろん行の方にラベルがあっても構わない.また両方とも行or列のインデックス指定(行番号指定,列番号指定)でも取り出せる.(loc[1:3, 4:6]は1行目~3行目かつ4列目~6列目を取り出す,という意味になる)

4つ目のセル.X軸とY軸に,取り出した列を与えてグラフを描画する.

この場合X軸は年齢,Y軸は男性,女性,合計の年齢に対応する人口である. すでに前のセルで,列のデータを取り出してるので,それぞれの組を与えるだけで良い

# グラフ表示のライブラリmatplotlib.pyplot
import matplotlib.pyplot as plt

# X軸に年齢, Y軸にそれぞれの人数を入れる
# labelは凡例で表示される文字列.
# これをつけておかないと,どの色が何のデータを表しているのかわからなくなる.
plt.plot(age, population_man, label='man')
plt.plot(age, population_women, label='woman')
plt.plot(age, population_sum, label='sum')

# 凡例を表示させる
plt.legend()

#グラフを描画する
plt.show()

Google Colaboratoryでのグラフ描画の結果は以下のようになる.

matplotlib.pyplotで描画した結果

各種ライブラリのCSV読み込み機能

先の例ではpandasでCSVファイルを読み込んだが,実は他にも方法がある.

標準csvライブラリcsv.reader()による読み込み

Python3の標準CSVライブラリは,

  • 標準ライブラリのcodecs.open()関数で,文字コードを指定しつつファイルを開く
  • csv.reader()関数で「データ本体を含むインスタンス」にする.ちなみにイテレータのインスタンスである.
  • イテレータのインスタンスなので,next()関数,もしくはfor文で1行ずつ読んでいく.1行が1次元の文字列のlistとして出てくるので,処理をしつつ格納する.
  • 最後にファイルをclose()する.

という流れになる.

CSVファイル内に入っている「処理したい」ターゲットが文字列の場合,今の所この標準csvライブラリを使う方法が一番適当である.

numpy.loadtxt()による読み込み

numpyで読み込む場合,CSVの中身が数字である必要がある.

流れは標準csvライブラリと同じく,codecs.open()関数でファイルを開いて,2次元配列として一気に読み込み,close()する,という流れだが,「数字である」ことが前提なので,ヘッダ等の文字列で構成されている行をskiprows=行数と「何行読み飛ばすか」を指定してから読み込み始めなければならない.

またdelimiter='区切り文字'指定により,区切り文字を明示的に指定する必要がある.(CSVは区切り文字がTABのこともあるため)

pandas.read_csv()によるpandas.DataFrame形式での読み込み

先の通り,2019年現在は,pandasを使うのが一般的.

pandasも内部的にはnumpyの配列を使っているので,やはりnumpyの2次元配列で取り出すことができるが,もっと汎用性の高いDataFrame(データフレーム)という形式で読み込むことができる.(DataFrameがpandasの最大特徴である)

さらにpandasはCSVデータをDataFrameとして読み込む時,ヘッダを特殊扱いして文字列として取り出すことができる(もちろん後述のようにnumpyと同じくヘッダを最初から読み飛ばすしてもできる).

またnumpy.loadtxt()だと強制的にfloatのみになるが,pandas.read_csv()は全て整数だった場合intの配列を作ってくれる.

さらに,標準csvライブラリやnumpy.loadtxt()のようにファイルを最初にopenして読み込み後にcloseする必要がない,numpy.loadtxt()と同じく全てのデータを一気に2次元配列として持ってくることができるが,pandasのDataFrameは,列だけを1次元numpy配列として容易に抽出できる,キーワードを与えて行や列を持ってくることができる,等の特徴がある.

さらにCSV読み込みの時は,区切り文字は自動判定される.

pandasのDataFrameは,このように非常に便利なので,基本的にはpandasのread_csvを使うと良い.

pandas以外の方法のコード例

このコードは機能をまとめてクラス化しているので,Google ColaboratoryやJupyter Notebookのようにコード単位で逐次的に実行していくものではなく,複数を一気にダウンロードして描画するなど,バッチ的に一気に処理することができる.

もちろんpandasのみを使用するときでも,クラス化しておけば,このように一気に回すことができる.

# -*- coding: utf-8 -*-
# CSVPlotter.py

import csv, numpy, pandas
import urllib.request
import codecs

# ローカルのAnacondaのPythonでグラフウィンドウを出すためには,これが必要
from PyQt5.QtWidgets import QApplication
import matplotlib.pyplot


class CSVPlotter():
    def __init__(self, url_string):
        self.__url_string = url_string

    def download_csv(self, decoder):
        # 引数decoderで指定した文字コードのCSVファイルをダウンロードして,
	# UTF-8に直して保存する
        self.__gotten_http_response = urllib.request.urlopen(self.__url_string)
        if self.__gotten_http_response.code == 200:
        # コード200が返っていてきたら正常に取得できている
            print('Sucsess to get CSV from Internet.')
            # ダウンロードしてきたデータを一度ファイルに書き込む
            self.__downloaded_filename = 'downloaded.csv'
            downloaded_file = codecs.open(self.__downloaded_filename, mode='w', encoding='utf-8')
            downloaded_file.write(self.__gotten_http_response.read().decode(decoder))
            downloaded_file.close()
            print('Sucsess to write downloaded CSV data to file.')
        else:
            # それ以外のコードが返ってきたら,異常終了
            print('Cannot get CSV file. Code:', self.__gotten_http_response.code, 'Exiting...')
            sys.exit(1)

    def read_csv_with_csv(self):
        # 標準ライブラリのcsvを使ってCSVファイルを読み込む
        print('標準のcsvライブラリで読んで表示')
        csv_file = codecs.open(self.__downloaded_filename, mode='r', encoding='utf-8')
        self.__csv_data = csv.reader(csv_file)
        
        # header以外は2次元のlistで出てくる
        # 外側のlistがRow(行),
        # 内側のリストがColumn(列)でデータ型は文字列
        header = next(self.__csv_data)
        # 1行目(データ本体ではなくデータの内容を示すヘッダ)
        print(header)
        for a_row in self.__csv_data:
            print(a_row)

        csv_file.close()
            
    def read_csv_with_numpy(self):
        # NumPyライブラリを使ってCSVファイルを読み込む
        print('NumPyライブラリで読んで表示')
        # NumPyの読み込みデータはNumPyの2次元array(ndarray)でくるので,分解して表示
        # データ型はfloat
	csv_file = codecs.open(self.__downloaded_filename, mode='r', encoding='utf-8')
        self.__csv_data = numpy.loadtxt(csv_file, delimiter=',', skiprows=1)
	csv_file.close()

        # 区切り文字を','とし,1行目(ヘッダ)を抜かして読み込む
        for a_row in self.__csv_data:
            print(a_row)
      
    def read_csv_with_pandas(self):
        # Pandsライブラリを使ってCSVファイルを読み込む
        print('Pandasライブラリで読んで表示')
        data_frame = pandas.read_csv(self.__downloaded_filename)

	# 文字列ヘッダが含まれててもエラーを起こさない
	# ヘッダだけを取り出すことができる
        header = data_frame.columns.values.tolist() #headerは標準list
        self.__csv_data = data_frame.values 
        print(header)

        # Pandsの読み込みデータもNumPyの2次元arrayでくるので,分解して表示
        # データ型は,中身が整数ならintに変換される
        for a_row in self.__csv_data:
            print(a_row)

    def make_plot_csv_data_2d(self, x_index, y_index, line_color):
        # インデックスで指定されたX軸要素とY軸要素を使い
        # NumPyのndarrayに入れて
        # Matplotlib.plotで図を描く
        plot_x_array = numpy.array([]) #空のnumpy ndarrayを生成
        plot_y_array = numpy.array([])
        for a_row in self.__csv_data:
            if isinstance(a_row[x_index], str):
                # numpy ndarrayに追加していく
                plot_x_array = numpy.append(plot_x_array, float(a_row[x_index]))
            else:
                plot_x_array = numpy.append(plot_x_array, a_row[x_index])
            if isinstance(self.__csv_data[y_index], str):
                plot_y_array = numpy.append(plot_y_array, float(self.a_row[y_index]))
            else:
                plot_y_array = numpy.append(plot_y_array, a_row[y_index])
        # plotに使うデータを設定
        matplotlib.pyplot.plot(plot_x_array, plot_y_array, color=line_color)

    def plot_show(self, x_label, y_label):
        # plotの軸の名前を設定
        matplotlib.pyplot.xlabel(x_label, fontsize=10, fontname='serif')
        matplotlib.pyplot.ylabel(y_label, fontsize=10, fontname='serif')
        # plotする
        matplotlib.pyplot.show()


if __name__ == "__main__":
    # 八王子市年齢別人口
    # http://www.city.hachioji.tokyo.jp/contents/open/002/p005866.html
    csv_url = "http://www.city.hachioji.tokyo.jp/contents/open/002/p005866_d/fil/nenreibetsu_jinkou_2703.csv"
    csv_plotter = CSVPlotter(csv_url)
    csv_plotter.download_csv('shift-jis')
    csv_plotter.read_csv_with_csv()
    csv_plotter.read_csv_with_numpy()
    csv_plotter.read_csv_with_pandas()
    csv_plotter.make_plot_csv_data_2d(1, 2, 'gray')
    csv_plotter.plot_show('Age', 'Population Size')

応用 - バッチ的に複数のCSVを処理して描画する

上記でクラス化した処理を使って,一気に複数をダウンロードし,描画する.

八王子市の年齢別人口データを年度/四半期ごとに複数持ってきて,一つのグラフに色を変えてプロットしてみる.

前述のコード例では,標準CSVライブラリで出てくるCSVのデータ形式に合わせて行ごとに読み込んだが,Google ColabやJupyterのコードのように,pandasのCSVライブラリで読み込むと,1列のみ簡単に取り出せることができる.また,ヘッダの行数を指定して読み込まない,等の処理が可能である.

コード例

# -*- coding: utf-8 -*-
# MultiCSVPlotter.py

import numpy, pandas
import urllib.request
import codecs
from PyQt5.QtWidgets import QApplication
import matplotlib.pyplot

class MultiCSVPlotter():
    def __init__(self, url_string_list):
        self.__url_string_list = url_string_list
        # numpyのndarrayで多次元配列を作るためには
        # np.array([])で初期化せずに(→append()しても連結された1次元配列になってしまう)
        # np.arange(0, x軸の最大値+1)で0〜最大値までの配列を最初に作って(X軸になる),
        # そこにvstack関数を使ってY軸になる配列を足していく形を取ると良い
        self.__csv_data_array = numpy.arange(0, 121)
        # 他のものも一応初期化しておく
        self.__column_index = 0

        return None

    def set_referring_column_index(self, column_index):
        # CSVファイルの何列目を見るか 0(1列目)~
        self.__column_index = column_index
        return self

    def download_csv_then_push_stack(self, decoder):
        # ShiftJISのCSVファイルのURL配列をダウンロードして,
        # UTF-8に直して一時保存してから,
        # pandasのCSV読み込み機能を使って読み込み,
        # ndarrayに2次元配列として入れていく
        for a_url in self.__url_string_list:
            self.__gotten_http_response = urllib.request.urlopen(a_url)
            if self.__gotten_http_response.code == 200:
                # コード200が返っていてきたら正常に取得できている
                print('Sucsess to get CSV from Internet.')
                # ダウンロードしてきたデータを一度ファイルに書き込む
                self.__downloaded_filename = 'tmp_downloaded.csv'
                downloaded_file = open(self.__downloaded_filename, mode='w', encoding='utf-8')
                downloaded_file.write(self.__gotten_http_response.read().decode(decoder))
                downloaded_file.close()
                print('Sucsess to download CSV data to file: ' + a_url)
                print('Now start to read CSV as ndarray with pandas library.')
            else:
                # それ以外のコードが返ってきたら,異常終了
                print('Cannot get CSV file. Code:', self.__gotten_http_response.code, 'Exiting...')
                sys.exit(1)

            # Pandsライブラリを使ってCSVファイルを読み込む
            # ヘッダは読み込まない指定(header=-1)
            # ヘッダは「1行」である指定(skiprows=1)
            data_frame = pandas.read_csv(self.__downloaded_filename, header=-1, skiprows=1)
            
            # まず取得した2次元配列の中から,column_indexで指定した列だけを抜き出す
            # pandasは取得したCSV,ここではdata_frameに対して,
	    # data_frame[列idnex]でその列のみの
            # 1次元配列をとり出せる.
            populationSizeArray = data_frame[self.__column_index]
            # できた1次元配列を,最初にX軸の配列で初期化した配列にvstack(())する.
            # vstackは2重括弧であることに注意
            # ただのstackでは同じ形(行と列が同じ量)の配列or行列同士しか足せないが,
            # vstackは要素数が同じ1次元配列をどんどん重ねていき
            # 多次元配列を作ることができる
            self.__csv_data_array = numpy.vstack((self.__csv_data_array, populationSizeArray))
            # ちなみにvstackの他に,hstack, column_stack, row_stackなどがある
            # これを,CSVファイル数分だけ回す
            print('Sucess to push new CSV column to stack')
        return self
        # return selfしないと,最後に実行した行の結果がreturnされるはず
        # この関数の場合はそれでもいいのだけれど
            
    def make_plot_csv_data_2d(self, line_color_list, line_legend_list):
        # Matplotlib.plotで図を描く
        # indexが1~shape(行列の次元数のtupleが得られる,ここでは(9, 121))[0]まで
        for index in range(1, self.__csv_data_array.shape[0]):
            # plotに使うデータを設定
            matplotlib.pyplot.plot(self.__csv_data_array[0], self.__csv_data_array[index], color=line_color_list[index-1], label=line_legend_list[index-1])
        #線に付与したlabel(凡例)を表示する
        matplotlib.pyplot.legend()

    def plot_show(self, x_label, y_label):
        # plotの軸の名前を設定
        matplotlib.pyplot.xlabel(x_label, fontsize=10, fontname='serif')
        matplotlib.pyplot.ylabel(y_label, fontsize=10, fontname='serif')
        # plotする
        matplotlib.pyplot.show()


if __name__ == "__main__":
    # 八王子市年齢別人口
    # http://www.city.hachioji.tokyo.jp/contents/open/002/p005866.html
    csv_url_list = list([])

    # matplotlibで使える色のリスト
    # http://matplotlib.org/examples/color/named_colors.html
    # 一応16進数カラーコードでも指定はできる
    line_color_list = list([])

    # 凡例(線の色がどの年のデータを表しているか)
    line_legend_list = list([])

    # H25 6月
    csv_url_list.append('http://www.city.hachioji.tokyo.jp/contents/open/002/p005866_d/fil/nenreibetsu_jinkou_2506.csv')
    line_color_list.append('blue')
    line_legend_list.append('H25.6')

    # H25 9月
    csv_url_list.append('http://www.city.hachioji.tokyo.jp/contents/open/002/p005866_d/fil/nenreibetsu_jinkou_2509.csv')
    line_color_list.append('green')
    line_legend_list.append('H25.9')

    # H25 12月
    csv_url_list.append('http://www.city.hachioji.tokyo.jp/contents/open/002/p005866_d/fil/nenreibetsu_jinkou_2512.csv')
    line_color_list.append('red')
    line_legend_list.append('H25.12')

    # H26 3月 (「平成25年3月末日」tと書いてあるのは25年「度」3月末……の意味らしい)
    csv_url_list.append('http://www.city.hachioji.tokyo.jp/contents/open/002/p005866_d/fil/nenreibetsu_jinkou_2503.csv')
    line_color_list.append('cyan')
    line_legend_list.append('H26.3')

    # H26 6月
    csv_url_list.append('http://www.city.hachioji.tokyo.jp/contents/open/002/p005866_d/fil/nenreibetsu_jinkou_2606.csv')
    line_color_list.append('magenta')
    line_legend_list.append('H26.6')

    # H26 9月
    csv_url_list.append('http://www.city.hachioji.tokyo.jp/contents/open/002/p005866_d/fil/nenreibetsu_jinkou_2609.csv')
    line_color_list.append('yellow')
    line_legend_list.append('H26.9')

    # H26 12月
    csv_url_list.append('http://www.city.hachioji.tokyo.jp/contents/open/002/p005866_d/fil/nenreibetsu_jinkou_2612.csv')
    line_color_list.append('black')
    line_legend_list.append('H26.12')

    # H27 3月 (同様にH26年「度」3月末の意味のようだ)
    csv_url_list.append('http://www.city.hachioji.tokyo.jp/contents/open/002/p005866_d/fil/nenreibetsu_jinkou_2703.csv')
    line_color_list.append('gray')
    line_legend_list.append('H27.3')

    csv_plotter = MultiCSVPlotter(csv_url_list)
    csv_plotter.set_referring_column_index(2)
    csv_plotter.download_csv_then_push_stack('shift-jis')
    csv_plotter.make_plot_csv_data_2d(line_color_list, line_legend_list)
    csv_plotter.plot_show('Age', 'Population Size')