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

CSVデータからの単純線形回帰分析

Jupyter NotebookとGoogle Colaboratory

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

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

気象庁オープンデータ

気象庁|過去の気象データ・ダウンロード」ページから,必要なデータを絞ってCSV形式でダウンロードすることができる.これを分析対象にする.

官公庁らしく,文字コードがSHIFT-JISなので注意する.

線形単純回帰分析

xとyの関係が例えば「y = ax + b」という「線形」の「モデル」すなわち一次関数の直線で表現できると仮定し,実際に観測されたデータを用いて,その直線を表現するaとbを求めることを,線形回帰分析という.(xを説明変数(Explanatory Variable)もしくは独立変数,yを目的変数(Response Variable)もしくは従属変数,応答変数などと呼ぶ)

実際には「y = ax1 + bx2 + cx3 + dx4 + e」というモデルも考えられる.これは説明変数が4つ,目的変数が1つの場合である.

説明変数が1つの時「単純回帰分析」と呼び,説明変数が2以上の場合は「重回帰分析」と呼ぶ.とりあえず本項では「単純回帰分析」,特に「y = ax + b」のモデルで捉える単純線形回帰分析を扱う.

(なぜ「特に」かというと,「y = ax^2 + b」というモデルも,求めるべき係数aが説明変数「x二乗」に対して線形であることから,こういうモデルも「単純線形回帰分析」と呼べる,という理由による.この場合は「x二乗」をそのまま説明変数として使うため,計算上そのまま扱えるように説明変数の配列をを二乗になっている状態で作成しておく.)

最小二乗法

現実に観測されたデータには「ノイズ」が乗っている.すなわちどんなデータも綺麗なy = ax + bの直線上にあることは殆どなく,そこからズレていることが前提である.そこで観測された全てのデータの,直線で表されるyの量との「ズレ」の総量を最小にする「最小二乗法」が一般的に用いられる.

Scikit-LearnというPythonのライブラリは,回帰分析などの機械学習アルゴリズムを豊富に備えており,簡単に学習ができるようになっている.目的変数と説明変数の観測値のデータを表で作り,それをfit(説明変数, 目的変数)とすることで,簡単にこのような機械学習を実行することができる.

そのためにはpandasのDataFrameという形式でCSVなどの表データを扱うと都合が良い.

ちなみに単純回帰分析の英語名は「Ordinary Least Squares」であり略称がOLSである.

対象データ

上記の気象庁のオープンデータサイトより,東京都八王子市の1977年〜2018年の8月の月平均気温,日の最高気温の月平均,日の最低気温の月平均,3つが入っているデータをダウンロードした.八王子市でも夏の温暖化が進んでいるとすれば,説明変数xを年(データがある1977年〜2016年を0〜にする),応答変数yを日中最高気温の月平均として,単純線形回帰分析をかけると「y = ax + b」が成立するかもしれない.

気象庁からダウンロードしたCSVはこんな感じである

コード例

最初のセル

from google.colab import files
uploaded = files.upload()

これを実行すると,ファイルを読み込んでくれるようになるので「ファイルを選択」してアップロードすると,以下のようにGoogle Colaboratory内に読み込まれる.

セル1の実行結果

2つ目のセル

import numpy as np
import pandas as pd
from pandas import Series, DataFrame

df = pd.read_csv('hachioji-temp-data.csv', encoding='SHIFT-JIS', header=2)
# PandasのDataFrameという形式で読み込まれる
# 文字コードはSHIFT-JIS, 官公庁のオープンデータや,歴史が古い企業のデータはエクセルから書き出すものが多いからか,SHIFT-JISが多い.
# このheader=2は頭から2行分は不要な情報なので読み飛ばす,という意味

df.head()
# チェック用として,頭の部分だけ表示

# DataFrameでは,この状態で一番上の行が「列の名前」として認識され,操作することができる.
# 列の名前になっているので,この行を行番号では操作できなくなる.

実行すると,CSVが読み込まれる

セル2の実行結果

3つ目のセル.余計なものが1行入ってたので削除.

# 列の名前が入っている行の下に余計なものが一つあったので(行番号0になる),削除
df = df.drop(0) #0番目の行を削除する
df.head()
セル3の実行結果

4つ目のセル.

# 「日最高気温の平均(℃)」列のみを抽出
# 1次元目の「:」は行のインデックスが「0~あるだけ」
# つまり全ての行で,このラベルがついた列を抽出,という意味になる
df = df.loc[:, ['日最高気温の平均(℃)']]
df.head()
セル4の実行結果

5つ目のセル.データの形状を読み込めるように加工する.

今回は1977年〜2918年のデータなので説明変数である「年」と目的変数である「月間の最高気温の平均」は42個(42年分)ある.したがって,ここで説明変数と目的変数に与える「配列の形状」が(42, 1)になっている必要がある.この「形状」は,DataFrameクラスのインスタンスの中のshapeという変数に格納されている.

matplotlib.pyplotというライブラリを使って,その加工した説明変数と目的変数をプロットしてみる

import scipy
import sklearn
from sklearn import linear_model

# 回帰分析するクラスのインスタンスを作る
clf = linear_model.LinearRegression()


# 説明変数は,その西暦年年にする
# 今は説明変数が「西暦年」1つなので,西暦年1次元のNumpy配列(1977~2018)を作る.
# np.arange(a, b)は,aからb-1までの整数のNumpy配列を作る関数
# 重回帰分析をするためには,複数の説明変数が必要となるので,2次元配列を取ることになる.
# そこでreshape(42, 1)を使って,無理やり2次元のNumpy配列にして格納する.
X = np.arange(1977, 2019).reshape(42, 1)

# 目的変数を 「日最高気温の平均(℃)」にする
# Nanが入っていることがあるので,Nanが入っていたらその行を取り除くコード
# 入っていないこともある
# 入っていたら当然行が減ってズレるので,
# 説明変数Xの西暦の方も適切にその行を取り除く必要がある.
y_temp = np.logical_not(pd.isnull(df))
mask = np.logical_and(np.cumsum(y_temp)!=0, np.cumsum(y_temp[::-1])[::-1]!=0)
df = df.loc[mask['日最高気温の平均(℃)'].tolist()]

# 目的変数は「1つ」なので,必ず1列になるので,「データは1次元」,「全体として2次元」であることを確認する
# DataFrameのvaluesという変数には,Numpyの2次元配列が入っているので,これを用いる
Y = df.values

# 説明変数と目的変数の行列の形状を確認する
#(現在は説明変数一つなので,同じになっている必要がある)
print('説明変数Xの形状', X.shape) #(サンプル数, 説明変数の数(今は1))になっている必要がある
print('説明変数Yの形状', Y.shape) #目的変数なので, (サンプル数, 1)になっている必要がある

#一度グラフに表示してみる
import matplotlib.pyplot as plt
plt.plot(X, Y)
plt.show()
セル5の実行結果

6つ目のセル.いよいよ学習して,結果を同じく表示する.

学習した後は,そのインスタンスのpredict()関数に,説明変数を突っ込めば,予測した値が出てくるので,そちらを観測値の後にプロットしている.

# 回帰分析を走らせる(学習する)
clf.fit(X, Y)

# 結果を出力していく
# 回帰係数(傾き)
print('回帰係数(傾き):', clf.coef_)
 
# 切片 (誤差)
print('切片:', clf.intercept_)
 
# 決定係数
print('決定係数', clf.score(X, Y))
# データにどのぐらいフィットしているかを表す値

# グラフを表示
# まず観測値を再度プロットする
plt.plot(X, Y)
# 次に回帰分析した結果をプロットする
# 回帰分析インスタンスのpredict関数に説明変数Xを入れると,予測した目的変数を出してくれる
predictedY = clf.predict(X)
# それを表示
plt.plot(X, predictedY)
# 表示
plt.show()
セル6の実行結果

結果

式の結果は「y = 0.05362 x + (-76.3767)」であった.プロットしてみると,なんとなく合っているような気がする.

(切片が大きくマイナス?と考えたかもしれないが,説明変数が1977からスタートしていることに注目.この切片は0の時,つまり西暦0年の時の8月の気温に相当する.実際にそんなことはないと思うので,産業革命以降に気温の上昇が始まった,つまり産業革命以降の実際の記録から回帰分析すれば,もっと傾きが大きくなる,と考えるのが妥当だろう)

決定係数とは「予測がどれだけ観測データにフィットしているか」を表す値である. 今回のデータは気温であり,気温の変動は周期性があり毎年の変動が多いので,今回は決定係数は低くても,全体的にみてこの傾きが観測データになんとなく沿っていることがわかれば十分である.

今回は「なんとなく合っているような気がする」だったが,高機能なライブラリは「統計的な正当性」(例えば検定など)も同時に行ってくれるため,論文のデータなどに使う場合は,そちらを用いる,Numpyで別に検定を行う,などを行う必要がある.

例えば,分析機能に特化しているライブラリstatsmodelsは,検定結果まで出してくれる.(参考:Python3で線形モデルによる回帰分析とプロット | 全人類がわかる統計学

statsmodels で線形回帰 - Qiitaというページでは,二次関数を線形回帰分析で解くための「線形基底関数モデル」に関する説明もある.


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

CSVの取得とPlot

各種ライブラリの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を使うと良い.

コード例

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

import csv, numpy, pandas
import urllib.request
import codecs
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のデータ形式に合わせて行ごとに読み込んだが,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')

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

Python3の基礎とデータ処理

概要

近年PythonがそれまでのMATLABやR言語に代わって,データ処理に使われることが多くなった.また海外では教育用言語として90年代末から普及が始まり,現在様々なフレームワークが開発されているWebサーバ側をはじめとしたサーバ用途と,通常のデスクトップのスクリプト処理用途,両用のLL(Lightweight Language)として使われている.

Pythonの特徴として「Python本体はスクリプト言語だが,ライブラリをC言語で実装しPythonにAPIを用意することで,高速に実行可能」という点がある.近年ではデータ解析や機械学習などで頻繁にPythonが使われているが,これはこの「Cで書かれた高速ライブラリをPythonで実行」というスタイルに依るものである.

現在のPythonは大きく分けてVer.2.x系とVer.3系の2つが使われているが,本授業ではPython3を対象とする.(OSX標準のPythonは2.7なので,授業の下準備としてAnacondaをインストールしておくこと)

コンテンツ

  1. Pythonの基本と配列(list)の取り扱い
  2. 関数とクラスの定義
  3. 文字列の取り扱い
  4. lambda式,高階関数,内包表記
  5. HTMLParserクラスによるWebページの取得と分析
  6. Numpyと配列(array)とmatplotlib
  7. CSVの取得とPlot
  8. CSVデータからの単純線形回帰分析
  9. CSVデータからの重回帰分析

授業の下準備

Windows

Anacondaを使う場合

Anacondaは現在標準となっているPythonのデータアナリシスディストリビューションである.こちらを使うのが一番楽.

Anacondaダウンロードページから,「64-Bit Graphical Installer」をダウンロードする.

インストール画面1

「Next」

インストール画面2

「I Agree」

インストール画面3

「All Users」の方が実は楽.管理者パスワードがわからないときには「Just Me」

インストール画面4

インストール場所に半角スペースなどが含まれているとエラーになるので,そのまま.もしくはCドライブ直下にするなど.

インストール画面5

上の方(システムパスに追加)は,システム的に混乱する可能性があるので外して置いた方がいい.

逆に下はVisual Studio等のWindowsコンパイラのみに設定を与えるもので,GPUの利用などを考えているなら,チェックを入れておく.(基本的にチェックを入れて置いて大丈夫)

インストール画面6

インストールが進む.

インストール画面7

終わったら「Next」

インストール画面8

Visual Studio Codeというエディタを追加インストールするか訊かれる.Visual Studio Communityが既に入っていたり,VS Codeを使わないなら「Skip」

インストール画面9

「Finish」

WindowsでMSYSを使う場合

CやC++のツールと連携させたい場合のみ(VSのコンパイラが必要なCUDA系は除く),Cコンパイラとセットになるこちらを使う.MSYSを使ってインストールしたPythonは文字コードがWindowsのもの縛りになるので注意.データ分析ならAnacondaの方が使い勝手は良い.

まっさらな状態からのMSYS2+GCCでWindows上にC言語とC++の環境を整えるの通りにコンパイラ環境を整える.

その後「MSYS2 MSYS」ショートカットで起動する端末エミュレータから,

$ pacman -S mingw64/mingw-w64-x86_64-python3
$ pacman -S mingw64/mingw-w64-x86_64-python3-numpy
$ pacman -S mingw64/mingw-w64-x86_64-python3-scipy
$ pacman -S mingw64/mingw-w64-x86_64-python3-matplotlib
$ pacman -S mingw64/mingw-w64-x86_64-python3-pandas

の五つのパッケージをインストールしておく.依存関係にある必要なライブラリ(主にQt関係)も全て一緒に入ってくれる.(64bit Windowsの場合)

Pythonの実行時には,MSYS2 MSYSから,

 $ /mingw64/bin/python3 MyPythonCode.py

と実行する.

OSX, macOS

OSX,macOSはPython本体とnumpy, scipy, matplotlibは標準で入っているのだが,バージョンが2.7である.HomebrewなどでPython3を入れるという方法もあるが,Homebrewでインストールを行うとシステム全体に影響を及ぼし,元々入っていたPython2.7とごちゃ混ぜになりシステムにダメージを与える可能性が高い.

そこでデータ処理に特化したPythonのパッケージであるAnacondaを「HDDのアプリケーション」以下にインストールして使う.Anacondaは/Applications/anacondaフォルダ内で独立したファイルツリーを持つ.

Python3を使いたい場合はAnacondaのPythonを明示的に実行してやる形にすれば,通常のシステムの処理に必要なPythonは元々システムに入っていた2.7がそのまま使われることになる.この方法で,システムに必要なPython2.7と,プログラミング言語として使いたいPython3系を,安全に同居させることができる.

Anacondaのダウンロードページの「Download for macOS」から「Python 3.6 version」の「Graphical Installer」をダウンロードする.

ダウンロードされた.pkgを実行して,ライセンスに同意し,「インストール先を変更」して「特定のディスク」の「アプリケーション」フォルダにインストールする.そうするとマシンの「/Applications」に「anaconda」フォルダ内にanacondaがインストールされ,Terminal.appから実行するPythonはこの中のPythonを使うように設定される.

Anaconda3のインストールパッケージ一覧の画面 Anaconda3のライセンス表示 Anaconda3のライセンス同意 Anaconda3「インストール先を変更」 Anaconda3「特定のディスクにインストール」を選択 Anaconda3メインのシステムHDDが選択されていることを確認し「フォルダを選択」 Anaconda3 フォルダを選択.ハードディスク直下の「アプリケーション」 Anaconda3 選択終了「続ける」

「このユーザがTerminal.appを開いた時はAnacondaのPythonを使う」という設定が記述されている~/.bash_profileからexport PATH=/Applications/anaconda...という行を削除しておき,Python3を実行する時は「Terminal.app」から,

 $ /Applications/anaconda/bin/python MyPythonCode.py

と「Anacondaのpython」を明示的に実行するようにしておくと,システム的に混乱が起きない.

OSXのPython2.7には,この授業の目的である基本的なデータ解析で使うnumpy, matplotlib, scipyも標準で含まれているのだが,matplotlibが使うGUI部品(システム標準はおそらくTk, Anacondaや上記WindowsのMSYS2環境ではQt5)や,リストを使ったイテレータの言語仕様などが違うため,Python2.7と3で互換性がないプログラムもある.

Anacondaの環境を作って実行環境のターミナルを開く

Pythonはライブラリがたくさんあり,衝突するものもある.Anacondaは複数の「環境environment」を作り,「環境」ごとにライブラリをインストールすることができるため,ライブラリの衝突を防ぐことができる.

ここでは授業用の環境「for_class」を作ってみる.

環境を作る

「Environment」タブにして,下の「Create」ボタン.

環境の名前をつける

環境の名前(ここでは"for_class")をつけて,「Create」.

環境が作られている

環境が作られている.

再生ボタンからOpen Terminal

環境の作成が終わったら,作成した環境の再生ボタンから「Open Terminal」.

Terminalが開いて実行できる

Terminalが開いて実行できるようになるので,プログラムが置いてあるフォルダにcdコマンドで移動して,

python 実行したいpythonプログラムファイル.py

で実行できるようになる.

Anaconda-Navigatorでの追加ライブラリのインストール

AnacondaはGUIでライブラリが追加インストールできる.ここでは例としてTwitterのAPIを使うために必要なOauth認証に必要なrequests_oauthlibをインストールする.

conda-forgeというレポジトリ(AnacondaではChannelと呼称する)がよくメンテされているようなので,それを追加して,Anacondaから使えるようにする.

まず管理者権限をもつユーザでAnaconda-Navigatorを起動する.

管理者権限を持つユーザでAnaconda-Navigatorを起動する

「Environment」(環境)を選択→「当該プロジェクトで使っているAnacondaの環境(ここではrootだが,基本的には前述の通りプロジェクトごとに環境を作る)」が選択されていることを確認→「Channels」ボタンを押す.

環境を選択しChannelを追加する

ミニウィンドウが開くので「Add」

ミニウィンドウが開く

新たなフィールドに「conda-forge」チャンネルを入力.

新たなフィールドに「conda-forge」チャンネルを入力

追加したら「Update channels」

「Update channels」

もとの画面に戻るので「Update Index」すると,下のバーでIndexをアップデートしている進行状況が表示される.

「Update Index」

一番左のプルダウンメニューから「Not Installed」(まだインストールされていないライブラリの検索)→一番右のボックスに「ライブラリの名前」(ここでは「requests-oauthlib」)を入力すると,下のフィールドに検索結果が出てくる.

「Not Installed」→「ライブラリの名前入力」→検索結果がでる

ライブラリにチェックを入れて,「Apply」

ライブラリにチェックを入れて「Apply」

進行状況のミニウィンドウが出る.

進行状況のミニウィンドウ

入れたいライブラリに必要なライブラリ(依存関係と呼称する)が全て並んで出てくるので「Apply」.

依存関係の表示

プルダウンメニューを「Installed」にして,インストールされていることを確認する.

「Installed」で確認.

以上である.Environment→rootの再生マークからターミナルを呼び出しても使えるし,root環境に入れておけば,通常のターミナルから/Applications/anaconda/bin/python3でPythonインタプリタを呼び出した時もライブラリが使える.

Anacondaで環境をたくさん作り,使用するライブラリを切り替えることもできる.


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

CSVデータからの線形重回帰分析

この内容は前期試験の課題である

気象庁オープンデータ

前回やったのと同じ「気象庁|過去の気象データ・ダウンロード」ページから,必要なデータを絞ってCSV形式でダウンロードすることができる.今回は複数の観測地点のデータを持ってきて,年度と緯度と8月の日中最高気温の月平均を見てみる.

重回帰分析

重回帰分析とは,説明変数が2変数以上の回帰分析である.多変量解析と呼ばれる分析手法群の中の一つの手法である.重回帰分析は,モデルが線形である場合,つまり簡単に言えば「y = ax1 + bx2 + c」のaとbとcを求めることに相当する.

重回帰分析の時,モデルの中の説明変数x1やx2は,実は(x1)^2など二乗の場合でも分析できる.あくまで係数との関係が線形であれば線形回帰として求められる.この場合,説明変数を入力する際にあらかじめ二乗したデータを作っておいて,それを入力する作業が必要になる.

Scikit-Learnでの重回帰分析

説明変数の単純回帰分析と同じくScikit-Learnライブラリでは,説明変数が多い場合の重回帰分析を同じようにで行うことができる.違うのは説明変数の数であり,前回の単純回帰分析では説明変数,目的変数に与えるNumpy配列のshapeは,両方とも(41, 1)であった.

重回帰分析の場合,説明変数のshapeが説明変数の個数の分だけ大きくなる.

つまり,y = ax1 + bx2 + cx3 + d という式の, a, b, c, dを求めたい時の,説明変数のshape(41, 3)ということになる.

DataFrameで複数の列を持ってくる

方法は「列の名前」をカンマで区切って持って来れば良い.

df = pandas.read_csv('CSVファイル名')
df = df.loc[:, ['平均気温(℃)', '日最高気温の平均(℃)']]

とすれば,平均気温の列と日最高気温の平均の2つの説明変数を格納したDataFrameができ,このDataFrameのshape(41, 2)となる.

後は全く同じように回帰分析クラスのインスタンスにこの説明変数と目的変数でfit()してやれば良い.

課題

複数の観測点の観測データをもとに,説明変数1:観測年,説明変数2:観測された地点の緯度,日中平均気温の月平均を目的変数として,重回帰分析を行い,プロットするPythonコードを書け.


[Work/Class/Web基礎/3_P5]

P5.jsとJavaScriptの機能の連携とJavaScriptの関数

P5.jsとJavaScript

P5.jsはただのJavaScriptのライブラリなので,Processingの機能からJavaScriptの機能を使うことができる.

Processingで使っていたマウスイベントを取得する関数ブロック「void mousePressed(){ }」も,そのまま「function mousePressed(){ }」として利用でき,またその時点でのマウス座標を示す変数「mouseX」「mouseY」もそのまま利用できる.

以下の例では,JavaScriptの強制リンク飛ばし機能「location.href = "飛ばしたいURL";」を,mousePressed内から呼び出して,P5でのリンクを実現させている.

以下は「P5js21_withJSFunction.html」である.

<!DOCTYPE html>
<html lang="ja">
<head>
   <meta http-equiv="X-UA-Compatible" content="IE=edge" />
   <meta charset="utf-8" />
   <title>P5.jsからJavaScriptの機能を呼び出す</title>
   
   <!-- 絶対パス指定でネットから直接ライブラリを読み込む -->
   <script type="text/javascript" 
              src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.8.0/p5.js">
   </script>
   
   <!-- 自分のP5.jsコードを読み込む -->
   <script type="text/javascript" src="./P5js21_withJSFunction.js">
   </script>
</head>
<body>

   <div id="P5Canvas">
   </div>

</body>
</html>

以下は対応する「P5js21_withJSFunction.js」である.

//グローバル変数
window.canvas;

function setup(){
    //size(640, 480);に相当する
    window.canvas = createCanvas(640, 480);
    //ただしこの命令でhtml上にcanvasタグが挿入される.
    canvas.parent("P5Canvas");

    //カラーモードはRGBにして値の範囲を0~255に設定する.透過度も最大値255.
    colorMode(RGB, 255, 255, 255, 255);
}

function draw(){
    noStroke(); //枠線なしの描画モード

    fill(255, 255, 255); //白
    rect(0, 0, 640, 480); //画面のリフレッシュ(アニメーションの時のお約束)

    fill(255, 0, 0); //左半分を赤の四角で埋める
    rect(0, 0, 320, 480);

    fill(0, 255, 0); //右半分を緑の四角で埋める
    rect(320, 0, 640, 480);
}

function mousePressed(){
    //クリックイベントが発生した時
    if(mouseX > 320){
	//もしクリックした場所が右半分(緑の場所)だったら
	location.href = "http://www.yahoo.co.jp";
	//Yahooジャパンのトップページに飛ばす
    }
    else{//それ以外の場所をクリックしたら
	location.href = "http://www.google.com";
	//Googleに飛ばす
    }

    //ただしマウスイベントはCanvasサイズの外でも有効になってしまうことに注意.
}

JavaScriptの関数

これまでも「function」は使ってきたが,これはJavaScriptで処理をまとめて記述するブロックで「関数」と呼ぶ.

関数は以下のように定義し,

function 任意でつける関数の名前(){ 処理のまとまり }

以下のように呼び出す.

任意でつける関数の名前();

以下の例では,Yahooに飛ばす関数gotoYahooと,Googleに飛ばす関数gotoGoogleを定義し,mousePressedで呼び出している.

この例のhtml「P5js22_withJSFunction_DefFunc.html」は以下の通り.

<!DOCTYPE html>
<!-- P5js22_withJSFunction_DefFunc.html -->
  <head>
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta charset="utf-8">
      <title>P5.jsからJavaScriptで定義した関数を呼び出す</title>

      <!-- 絶対パス指定でネットから直接ライブラリを読み込む -->
      <script type="text/javascript"
                 src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.8.0/p5.js">
      </script>
      
      <!-- 自分のP5.jsコードを読み込む -->
      <script type="text/javascript"
          src="./P5js22_withJSFunction_DefFunc.js">
      </script>
  </head>
  <body>

    <div id="P5Canvas">
    </div>

  </body>
</html>

対応する「P5js22_withJSFunction_DefFunc.js」は以下の通り.

var canvas;

function gotoYahoo(){
    //Yahooに飛ばす関数を定義する
    location.href = "http://www.yahoo.co.jp";
}

function gotoGoogle(){
    //Googleに飛ばす関数を定義する
    location.href = "http://www.google.com";
}


function setup(){
    //size(640, 480);に相当する
    canvas = createCanvas(640, 480);
    //ただしこの命令でhtml上にcanvasタグが挿入される.
    canvas.parent("P5Canvas");

    //カラーモードはRGBにして値の範囲を0~255に設定する.透過度も最大値255.
    colorMode(RGB, 255, 255, 255, 255);
}

function draw(){
    noStroke(); //枠線なしの描画モード

    fill(255, 255, 255); //白
    rect(0, 0, 640, 480); //画面のリフレッシュ(アニメーションの時のお約束)

    fill(255, 0, 0); //左半分を赤の四角で埋める
    rect(0, 0, 320, 480);

    fill(0, 255, 0); //右半分を緑の四角で埋める
    rect(320, 0, 640, 480);
}

function mousePressed(){
    //クリックイベントが発生した時
    if(mouseX > 320){
	//もしクリックした場所が右半分(緑の場所)だったら
	//Yahooジャパンのトップページに飛ばす関数を呼び出す
	gotoYahoo();
    }
    else{//それ以外の場所をクリックしたら
	//Googleに飛ばす関数を呼び出す
	gotoGoogle();
    }

    //ただしマウスイベントはCanvasサイズの外でも有効になってしまうことに注意.
}

画像をマウスクリックでDivを書き換え

document.getElementByIdを使って,マウスイベントクリックした場所に対応するメッセージを,特定のdivの中身を書き換えて表示する.

画像をマウスクリックでDivを書き換える動作確認ページ

<!DOCTYPE html>
<!-- image_ajax_rewrite_div.html -->
<html lang="ja">
  <head>
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta charset="utf-8" />
    <title>P5.jsで画像をAJAX的に読み込み,マウスイベントをマッピングする</title>

    <!-- ネットから直接P5.jsのライブラリを読み込む -->
    <script type="text/javascript" 
	    src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.8.0/p5.js">
    </script>

    <!-- 自分のP5.jsのコードを読み込む -->
    <script type="text/javascript" src="./image_ajax_rewrite_div.js">
    </script>

  </head>
  <body>
    <!-- ここにキャンバスが来る -->
    <div id="P5Canvas">
    </div>

    <div id="MessageDiv">
      <p>
	ここのDivの中身が書き換わる.
      </p>
    </div>
  </body>
</html>
// image_ajax_rewrite_div.js

// これから使うグローバル変数
window.canvas;
window.myImage;

function setup(){
    window.canvas = createCanvas(640, 480);
    window.canvas.parent("P5Canvas");

    colorMode(RGB, 255);

    // 画像をAJAXで読み込む
    // 画像はこのJSファイルを読み込んで実行するhtmlと同じ階層にある
    // ちなみにこの画像は640*480サイズに既に整えてある
    window.myImage = loadImage("./mini_kurone.png");
}

function draw(){
    noStroke();
    
    // 画像を左上0, 0を基準に描画
    image(window.myImage, 0, 0);

    // マウスポインタ用の半透明の丸を表示
    fill(255, 255, 255, 127);
    ellipse(mouseX, mouseY, 20, 20);
}


function mousePressed(){
    if(mouseY < 120){
	document.getElementById("MessageDiv").innerHTML = 
	    "<p>2歳まで野良だったので,耳カットがあるよ</p>";
    }
    else if(mouseY < 300){
	document.getElementById("MessageDiv").innerHTML =
	    "<p>つぶらなおめめがとってもキュート!</p>";
    }
    else{
	document.getElementById("MessageDiv").innerHTML = 
	    "<p>1本だけ白のおひげがあるのがキュート!</p>";
    }
}

関数の引数と返り値

関数には,引数(ひきすう,ひきかず)と呼ばれるオプションを与えることができる.この引数は「変数」である.

定義の時点で,

function 関数の名前(引数1, 引数2, 引数3....){ }

とし,functionの中でその引数の変数をそのまま用いることができる.引数の数は任意である(全く指定しない時は今までのように括弧の中を空にし,任意の数まで増やすことができる).関数を呼び出すときに,

関数の名前(引数1, 引数2, 引数3....);

と指定することで,引数を与えた状態で関数を呼び出すことができる.

また,関数の最後に「return 変数名;」と書くことで,関数の中で処理した値を,呼び出し元に返すことができる.これを「返り値」または「戻り値」と呼称する.

以下の例では,「gotoOtherWebSiteWithAlert」という,引数を二つ取る関数を一つ定義し,そこに与えられる引数siteFlagが0か1かでYahooに飛ばすかを決めている.

さらに飛ばしたサイトの名前を,変数「goingSiteName」に入れておき,関数の最後で,"に飛ばしました!"という文字列と連結し,変数「returnValue」に入れて,呼び出し元に返している.

以下がhtml「P5js23_withJSFunction_Argument.html」である.

<!DOCTYPE html>
<!-- P5js23_withJSFunction_Argument.html -- >
  <head>
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta charset="utf-8" />
    <title>P5.jsから関数を引数をつけて呼び出す</title>
    <!-- 絶対パス指定でネットから直接ライブラリを読み込む -->
    <script type="text/javascript"
        src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.8.0/p5.js">
    </script>

    <!-- 自分のP5.jsコードを読み込む -->
    <script type="text/javascript"
               src="./P5js23_withJSFunction_Argument.js">
    </script>
  </head>
  <body>

    <div id="P5Canvas">
    </div>

  </body>
</html>

以下が対応するJavaScriptファイル「P5js23_withJSFunction_Argument.js」である.

var canvas;

function gotoOtherWebSiteWithAlert(siteFlag, alertString){
    //alertString引数で渡された文字列のAlertを表示してから
    alert(alertString);

    //返り値を格納する変数.
    var goingSiteName; 
    if(siteFlag == 0){
	//もしsiteFlag引数が0だったらYahooに飛ばす
	location.href = "http://www.yahoo.co.jp";

	//返り値に文字列を入れる
	goingSiteName = "Yahoo";
    }
    else{
	//siteFlag関数が0以外だったらGoogleに飛ばす
	location.href = "http://www.google.com";

	//返り値に文字列を入れる
	goingSiteName = "Googl";
    }

    //返り値を返す

    var returningValue = goingSiteName + "に飛ばしました!";
    return returningValue;
}

function setup(){
    //size(640, 480);に相当する
    canvas = createCanvas(640, 480);
    //ただしこの命令でhtml上にcanvasタグが挿入される.
    canvas.parent("P5Canvas");

    //カラーモードはRGBにして値の範囲を0~255に設定する.透過度も最大値255.
    colorMode(RGB, 255, 255, 255, 255);
}

function draw(){
    noStroke(); //枠線なしの描画モード

    fill(255, 255, 255); //白
    rect(0, 0, 640, 480); //画面のリフレッシュ(アニメーションの時のお約束)

    fill(255, 0, 0); //左半分を赤の四角で埋める
    rect(0, 0, 320, 480);

    fill(0, 255, 0); //右半分を緑の四角で埋める
    rect(320, 0, 640, 480);
}

function mousePressed(){
    //クリックイベントが発生した時

    //どこのサイトに飛ばすかのフラグ変数を宣言する.
    var siteFlag = 0;
    //返り値を格納する変数を宣言
    var returnedValue;

    if(mouseX > 320){
	//引数を0と"Yahooへ飛ばします"という文字数にして,関数を呼び出す.
	returnedValue = gotoOtherWebSiteWithAlert(0, "Yahooへ飛ばします");
	//戻り値をreturnedValue変数の中に格納
    }
    else{//それ以外の場所をクリックしたら
	//引数を1と"Googleへ飛ばします"という文字数にして,関数を呼び出す.
	returnedValue = gotoOtherWebSiteWithAlert(1, "Googleへ飛ばします");
	//戻り値をreturnedValue変数の中に格納
    }

    //returnValueの中身をalertで表示
    alert(returnedValue);
}