Pythonで効率のよいデータクラス比較

# 目的
あるデータセットがある。
ここでは12ヶ月×47都道府県とする。値は気温とする（下の実験ではランダムなデータを用いた）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/662712/adcd7780-281a-2fbe-3012-ae9b251edcc9.png)

このデータを計算過程で何度か呼び出す。
このときに列を抽出する。例えば「東北」のデータを `df.loc[tohoku_list]` のように呼び出す。

それとは別に"規格化された"データも必要とするときがあるとする。
規格化とはデータの各列から、その列の平均を引いた値とする `df - df.mean(axis=0)` （このときも列抽出する）。


この状況でもっとも効率のよいデータクラスの作成方法を検討するため、以下の3パターンを考えた。

## 案1
+ 気温データ
+ 平均データ
を保持する。

```Python
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を使う

```Python
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
+ 気温データ
+ 規格化された気温データ
を保持する。

```Python
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
+ 気温データ
を保持する。

```Python
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関数では、列抽出した気温データから平均を計算し差分を返す。

#### メリット
+ 平均の計算時間＜列抽出の時間となるような小さいデータのみを取得する場合、有利になるかも？
+ メモリ使用量が小さく、クラス作成時のイニシャル時間が短い

#### デメリット
+ 毎回平均を計算するため、抽出する列数によっては時間が多くかかる

# 実際に実行し計算時間を調査する
## データの準備
```Python
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 = ["青森県", "岩手県", "宮城県", "秋田県", "山形県", "福島県"]
```

## データの準備にかかる時間
```Python
%%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を列をキーにして直接結合することができず、そのための変換に時間がかかっているかもしれない。

## 気温データの呼び出し
```Python
%%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% だが、この値がデータ量にどのオーダーで影響するのか調査する必要あり(増加時間はデータフレーム列サイズに比例＝パーセンテージ固定？)。

## 規格化データの呼び出し
```Python
%%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の検証を追加した