目的
あるデータセットがある。
ここでは12ヶ月×47都道府県とする。値は気温とする(下の実験ではランダムなデータを用いた)
このデータを計算過程で何度か呼び出す。
このときに列を抽出する。例えば「東北」のデータを df.loc[tohoku_list]
のように呼び出す。
それとは別に"規格化された"データも必要とするときがあるとする。
規格化とはデータの各列から、その列の平均を引いた値とする df - df.mean(axis=0)
(このときも列抽出する)。
この状況でもっとも効率のよいデータクラスの作成方法を検討するため、以下の3パターンを考えた。
案1
- 気温データ
- 平均データ
を保持する。
class data_class1:
def __init__(self, pref_data):
self.pref_data = pref_data
self.average = pref_data.mean(axis=0)
def get_data(self, column_list):
return self.pref_data[column_list]
def get_normalized_data(self, column_list):
return self.pref_data[column_list] - self.average[column_list]
get_data関数では、そのまま気温データから列抽出を行う。
get_normalized_data関数では、気温データ、平均データのそれぞれ列を抽出し差を計算する。
メリット
- 一番容易な実装
デメリット
- 規格化データの取得に列抽出(.loc)を2回呼び出す必要がある
案2
- 気温データ+平均データ
気温データに平均データを結合して、共通の列indexを使う
class data_class2:
def __init__(self, pref_data):
self.pref_data = pref_data
self.pref_data = pd.concat([pref_data.T, pref_data.mean(axis=0)], axis=1).T
def get_data(self, column_list):
return self.pref_data.iloc[:-1,:][column_list]
def get_normalized_data(self, column_list):
part_data = self.pref_data[column_list]
return part_data.iloc[:-1,:] - part_data.iloc[-1,:]
get_data関数では、気温データから列抽出を行い、最終行の平均データ以外を返す。
get_normalized_data関数では、全データを列抽出し、最終行以外から、最終行を引いたものを返す。
メリット
- 規格化列の取得時に列抽出が1度でよい
デメリット
- 気温データの取得、規格化データの取得どちらにも行選択が発生する
案3
- 気温データ
- 規格化された気温データ
を保持する。
class data_class3:
def __init__(self, pref_data):
self.pref_data = pref_data
self.normalized_data = pref_data - pref_data.mean(axis=0)
def get_data(self, column_list):
return self.pref_data[column_list]
def get_normalized_data(self, column_list):
return self.normalized_data[column_list]
get_data関数では、気温データから列抽出を行う。
get_normalized_data関数では、規格化されたデータから列抽出を行う。
メリット
- 気温データ、規格化されたデータどちらを取得する際にも、列抽出処理が1度でよい
デメリット
- メモリ使用量が大きい
案0
- 気温データ
を保持する。
class data_class0:
def __init__(self, pref_data):
self.pref_data = pref_data
def get_data(self, column_list):
return self.pref_data[column_list]
def get_normalized_data(self, column_list):
part = self.pref_data[column_list]
return part - part.mean(axis=0)
get_data関数では、気温データから列抽出を行う。
get_normalized_data関数では、列抽出した気温データから平均を計算し差分を返す。
メリット
- 平均の計算時間<列抽出の時間となるような小さいデータのみを取得する場合、有利になるかも?
- メモリ使用量が小さく、クラス作成時のイニシャル時間が短い
デメリット
- 毎回平均を計算するため、抽出する列数によっては時間が多くかかる
実際に実行し計算時間を調査する
データの準備
import pandas as pd
import numpy as np
np.random.seed(0)
pref_list = ["北海道", "青森県", "岩手県", "宮城県", "秋田県", "山形県", "福島県", "茨城県", "栃木県", "群馬県", "埼玉県", "千葉県", "東京都", "神奈川県", "新潟県", "富山県", "石川県", "福井県", "山梨県", "長野県", "岐阜県", "静岡県", "愛知県", "三重県", "滋賀県", "京都府", "大阪府", "兵庫県", "奈良県", "和歌山県", "鳥取県", "島根県", "岡山県", "広島県", "山口県", "徳島県", "香川県", "愛媛県", "高知県", "福岡県", "佐賀県", "長崎県", "熊本県", "大分県", "宮崎県", "鹿児島県", "沖縄県"]
pref_data = pd.DataFrame(np.random.rand(12,47), index=np.arange(1,13), columns=pref_list)
tohoku_list = ["青森県", "岩手県", "宮城県", "秋田県", "山形県", "福島県"]
データの準備にかかる時間
%%timeit
test1 = data_class1(pref_data)
# 184 µs ± 5.96 µs per loop (mean ± std. dev. of 7 runs, 10,000 loops each)
test2, 3, 0は省略
data_class | 実行時間(µs) |
---|---|
test1 | 184 ± 5.96 |
test2 | 751 ± 22.7 |
test3 | 301 ± 13.3 |
test0 | 149 ± 8.19 |
単純なtest0がもっとも高速であった。
一方で平均を結合するtest2は約5倍と大きな差があった。
PandasがDataFrameとSeriesを列をキーにして直接結合することができず、そのための変換に時間がかかっているかもしれない。
気温データの呼び出し
%%timeit
test1.get_data(tohoku_list)
# 268 µs ± 29.4 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
test2, 3, 0は省略
data_class | 実行時間(µs) |
---|---|
test1 | 268 ± 29.4 |
test2 | 277 ± 6.57 |
test3 | 251 ± 8.34 |
test0 | 246 ± 2.17 |
平均行を削除する必要のあるtest2がもっとも時間を消費した。
test1, 3に比べおよそ +18% だが、この値がデータ量にどのオーダーで影響するのか調査する必要あり(増加時間はデータフレーム列サイズに比例=パーセンテージ固定?)。
規格化データの呼び出し
%%timeit
test1.get_normalized_data(tohoku_list)
# 626 µs ± 25.6 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
test2, 3, 0は省略
data_class | 実行時間(µs) |
---|---|
test1 | 626 ± 25.6 |
test2 | 454 ± 5.8 |
test3 | 252 ± 18.3 |
test0 | 597 ± 14.2 |
test3 < 2 < 1の順は予想通りであった。
当然だが、test3は気温データ呼び出しと実行時間がほぼ同じであった。
test2はtest3の75%の時間で呼び出している。
test0は毎回平均を計算しているが、12×6データの計算においてtest1の事前に計算しておいたときより高速であることがわかった
(データサイズの大きさを変えたとき要検証)
気温データの呼び出し+規格化データの呼び出し
気温データと規格化データの呼び出し回数が等しいとすると、以下の比較順に高速になる。
data_class | 実行時間(µs) |
---|---|
test1 | 857 ± 10.4 |
test2 | 775 ± 16.5 |
test3 | 488 ± 14.4 |
test0 | 868 ± 15.1 |
test3がもっとも早いのは当然だが、test2 < test1 = test0 という結果になった。
これは気温データと規格化データの呼び出し割合が変化すると順番も変わりそうである。
メモリ使用量
pd.DataFrame.info()
を用い各クラス変数のメモリを調査した。
data_class | メモリサイズ(KB) | 備考 |
---|---|---|
test1 | 6.3 | pref_data(4.5) + average(1.8+) |
test2 | 4.9 | pref_data |
test3 | 9.0 | pred_data(4.5) + normalized_data(4.5) |
test0 | 4.5 | pref_data |
test1 より test2のほうがメモリ使用量が小さいという結果になった。
列indexを共通化しているだけお得である。
暫定結論
メモリ使用量を気にしなくてよい環境であれば3がよいが、
そのような環境でなくかつ気温データと規格化データの呼び出し回数が等しいのであれば、2の実装がよさそうである。
今後
データセットの大きさを変えて検証
抽出列の選び方で結果が変わるか検証
気温データの呼び出しと規格化データの呼び出しの比率を変えて分岐点をまとめる
追記
test0の検証を追加した