0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2023-05-22

目的

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

image.png

このデータを計算過程で何度か呼び出す。
このときに列を抽出する。例えば「東北」のデータを 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の検証を追加した

0
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?