pandasで1000万件のデータの前処理を高速にするTips集

# はじめに

当社にアルバイトに来ていた人（来春に新卒入社の予定）に「pandasを高速化するための情報は無いですか？」と尋ねられました。
このパッケージの使い方は多数の書籍やWebで体系立った記事で書かれています。
しかし、高速化に関しては体系的な情報源が思いつかなかったので、「実際に書いてみて、1つ1つチューニングするしかないです」としか答えられませんでした。
そこで、この方を始め、来春（2019年4月）にデータアナリストまたはデータサイエンティストになる新卒へ向けて、pandasの高速化に関する私の経験をTips集にしてお伝えしたいと思います。

この記事は今後も内容を充実させるために、Tipsを追加していきます。


# この記事を読んだ後にできるようになること

pandasでレコード数1000万件のデータでも1分以内で完了する前処理が書けるようになります。
その結果、1日中実行し続けなければならないような前処理を減らすことができ、分析に割く時間が増えます。


# 対象読者

本記事が想定している読者層は、次の3つのいずれかに該当する方々です。

- 学部生や大学院生で将来データ分析を仕事にしたい
- アルバイトやインターンとして分析案件に携わっている
- すでに企業から内定をもらっていて、アナリストまたはデータサイエンティストとして就業予定


# 前提にするスキル

この記事では、以下について基本的な知識があることを想定しています。
これらの使い方は本記事では説明しませんので、ご注意ください。

- python
- pandas


# 1. ある1つのカラムによって新しい列をDataFrameに追加したい

## 使用するテストデータ

テストデータは`scikit-learn.datasets`の`california_housing`の特徴量を使います。
データの概略は以下の通りです。
データの詳細は[scikit-learnのCalifornia Housing dataset](https://scikit-learn.org/stable/datasets/index.html#california-housing-dataset)を参照してください。

- サンプルサイズ：20640
- 特徴量の数：8

各特徴量の概略

|Feature    |Description                |
|:----------|:--------------------------|
|MedInc     |median income in block     |
|HouseAge   |median house age in block  |
|AveRooms   |average number of rooms    |
|AveBedrms  |average number of bedrooms |
|Population |block population           |
|AveOccup   |average house occupancy    |
|Latitude   |house block latitude       |
|Longitude  |house block longitude      |


```python
import pandas as pd
from sklearn.datasets import fetch_california_housing

fetched_data = fetch_california_housing()
california_housing = pd.DataFrame(
    data=fetched_data['data'],
    columns=fetched_data['feature_names']
)
california_housing.head(5)
```

<div>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>MedInc</th>
      <th>HouseAge</th>
      <th>AveRooms</th>
      <th>AveBedrms</th>
      <th>Population</th>
      <th>AveOccup</th>
      <th>Latitude</th>
      <th>Longitude</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>0</th>
      <td>8.3252</td>
      <td>41.0</td>
      <td>6.984127</td>
      <td>1.023810</td>
      <td>322.0</td>
      <td>2.555556</td>
      <td>37.88</td>
      <td>-122.23</td>
    </tr>
    <tr>
      <th>1</th>
      <td>8.3014</td>
      <td>21.0</td>
      <td>6.238137</td>
      <td>0.971880</td>
      <td>2401.0</td>
      <td>2.109842</td>
      <td>37.86</td>
      <td>-122.22</td>
    </tr>
    <tr>
      <th>2</th>
      <td>7.2574</td>
      <td>52.0</td>
      <td>8.288136</td>
      <td>1.073446</td>
      <td>496.0</td>
      <td>2.802260</td>
      <td>37.85</td>
      <td>-122.24</td>
    </tr>
    <tr>
      <th>3</th>
      <td>5.6431</td>
      <td>52.0</td>
      <td>5.817352</td>
      <td>1.073059</td>
      <td>558.0</td>
      <td>2.547945</td>
      <td>37.85</td>
      <td>-122.25</td>
    </tr>
    <tr>
      <th>4</th>
      <td>3.8462</td>
      <td>52.0</td>
      <td>6.281853</td>
      <td>1.081081</td>
      <td>565.0</td>
      <td>2.181467</td>
      <td>37.85</td>
      <td>-122.25</td>
    </tr>
  </tbody>
</table>
</div>

```python
california_housing.describe()
```

<div>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>MedInc</th>
      <th>HouseAge</th>
      <th>AveRooms</th>
      <th>AveBedrms</th>
      <th>Population</th>
      <th>AveOccup</th>
      <th>Latitude</th>
      <th>Longitude</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>count</th>
      <td>20640.000000</td>
      <td>20640.000000</td>
      <td>20640.000000</td>
      <td>20640.000000</td>
      <td>20640.000000</td>
      <td>20640.000000</td>
      <td>20640.000000</td>
      <td>20640.000000</td>
    </tr>
    <tr>
      <th>mean</th>
      <td>3.870671</td>
      <td>28.639486</td>
      <td>5.429000</td>
      <td>1.096675</td>
      <td>1425.476744</td>
      <td>3.070655</td>
      <td>35.631861</td>
      <td>-119.569704</td>
    </tr>
    <tr>
      <th>std</th>
      <td>1.899822</td>
      <td>12.585558</td>
      <td>2.474173</td>
      <td>0.473911</td>
      <td>1132.462122</td>
      <td>10.386050</td>
      <td>2.135952</td>
      <td>2.003532</td>
    </tr>
    <tr>
      <th>min</th>
      <td>0.499900</td>
      <td>1.000000</td>
      <td>0.846154</td>
      <td>0.333333</td>
      <td>3.000000</td>
      <td>0.692308</td>
      <td>32.540000</td>
      <td>-124.350000</td>
    </tr>
    <tr>
      <th>25%</th>
      <td>2.563400</td>
      <td>18.000000</td>
      <td>4.440716</td>
      <td>1.006079</td>
      <td>787.000000</td>
      <td>2.429741</td>
      <td>33.930000</td>
      <td>-121.800000</td>
    </tr>
    <tr>
      <th>50%</th>
      <td>3.534800</td>
      <td>29.000000</td>
      <td>5.229129</td>
      <td>1.048780</td>
      <td>1166.000000</td>
      <td>2.818116</td>
      <td>34.260000</td>
      <td>-118.490000</td>
    </tr>
    <tr>
      <th>75%</th>
      <td>4.743250</td>
      <td>37.000000</td>
      <td>6.052381</td>
      <td>1.099526</td>
      <td>1725.000000</td>
      <td>3.282261</td>
      <td>37.710000</td>
      <td>-118.010000</td>
    </tr>
    <tr>
      <th>max</th>
      <td>15.000100</td>
      <td>52.000000</td>
      <td>141.909091</td>
      <td>34.066667</td>
      <td>35682.000000</td>
      <td>1243.333333</td>
      <td>41.950000</td>
      <td>-114.310000</td>
    </tr>
  </tbody>
</table>
</div>

ここでは、`HouseAge`を`young`, `middle`, `old`の3階級で分類した列を追加します。
`young`と`middle`の分岐、`middle`と`old`の分岐はそれぞれ`quantile`で1/3と2/3を計算した値を使います。

```python
california_housing.HouseAge.quantile([1/3, 2/3])
```

    0.333333    22.0
    0.666667    35.0
    Name: HouseAge, dtype: float64

```python
LOWER_SPLIT = 22.0
UPPER_SPLIT = 35.0

AGE_LABEL_YOUNG = 'young'
AGE_LABEL_MIDDLE = 'middle'
AGE_LABEL_OLD = 'old'
```


## 方法1　for文で新たな列を作る

まずはfor文で1行ずつ処理する場合を検証します。
この方法は、プログラミング初心者に多く見られる書き方です。
分かりやすいのですが、DataFrameの操作は重たいので、処理速度は遅くなります。

```python
def method1(data):
    _data = data.copy()
    _data.loc[:, 'HouseAgeClass'] = None
    for i in range(len(_data)):
        house_age = _data.iloc[i, :]['HouseAge']
        if house_age < LOWER_SPLIT: 
            _data.iloc[i, -1] = AGE_LABEL_YOUNG
        elif LOWER_SPLIT <= house_age < UPPER_SPLIT:
            _data.iloc[i, -1] = AGE_LABEL_MIDDLE
        else:
            _data.iloc[i, -1] = AGE_LABEL_OLD
    return _data
```


## 方法2 DataFrameのスライシングで新たな列を作る

次はDataFrameのスライシングを使って新たな列を作る方法です。
Rではお馴染みな方法なので、RからPandasに乗り換えたユーザーにとっては分かりやすいのではないでしょうか？  
私もPandasに乗り換えた当初はこの方法で列を作成していましたが、お世辞にも見やすいコードとは言えないので、最近はこの後で紹介するmapメソッドによる方法を使うことにしています。

```python
def method2(data):
    _data = data.copy()    # 元データに影響を与えないようにする
    _data.loc[(_data.HouseAge < LOWER_SPLIT), 'HouseAgeClass'] = AGE_LABEL_YOUNG
    _data.loc[(_data.HouseAge >= LOWER_SPLIT) & (_data.HouseAge < UPPER_SPLIT), 'HouseAgeClass'] = AGE_LABEL_MIDDLE
    _data.loc[(_data.HouseAge >= UPPER_SPLIT), 'HouseAgeClass'] = AGE_LABEL_OLD
    return _data
```


## 方法3 mapメソッドを使って新たな列を作る

最後にDataFrameのmapメソッドを使って作る方法です。
ヘルパー関数（この例では`create_house_age_class`）を定義する必要がありますが、可読性が良く、ヘルパー関数を修正するだけで分類の変更ができることが利点です。

```python
def method3(data):
    def create_house_age_class(age):
        if age < LOWER_SPLIT:
            return AGE_LABEL_YOUNG
        elif (LOWER_SPLIT <= age) and (age < UPPER_SPLIT):
            return AGE_LABEL_MIDDLE
        else:
            return AGE_LABEL_OLD

    _data = data.copy()
    _data.loc[:, 'HousingAgeClass'] = _data.HouseAge.map(create_house_age_class)
    return _data
```


## 検証結果

### 方法1〜3を比較

方法1は2万行のデータに対して21.6秒掛かっていて、方法2と方法3はどちらも1秒未満で終わっています。
また、グラフから、方法1はサンプルサイズの増加に対して指数関数的に増加する懸念がありますので、これ以上大きなデータに対して使うことはできません。

```python
from utilities.process_time import PandasProcessTimeMeasure

process_time_measure = PandasProcessTimeMeasure(
    data=california_housing,
    sample_sizes=[100, 500, 1000, 5000, 10000, 20000]
)
process_time_measure.set_method(name='method01', method=method1)
process_time_measure.set_method(name='method02', method=method2)
process_time_measure.set_method(name='method03', method=method3)
process_time_measure.measure_process_time_for_each_sample_sizes()
process_time_measure.plot_process_time()
```

![output_23_0.png](https://qiita-image-store.s3.amazonaws.com/0/218401/d7081b4a-8f9e-4caf-1245-9c639a791152.png)

```python
process_time_measure.process_time
```

<div>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>method01</th>
      <th>method02</th>
      <th>method03</th>
    </tr>
    <tr>
      <th>sample_size</th>
      <th></th>
      <th></th>
      <th></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>100</th>
      <td>0.038640</td>
      <td>0.004645</td>
      <td>0.000851</td>
    </tr>
    <tr>
      <th>500</th>
      <td>0.201697</td>
      <td>0.004845</td>
      <td>0.000944</td>
    </tr>
    <tr>
      <th>1000</th>
      <td>0.419440</td>
      <td>0.004602</td>
      <td>0.001031</td>
    </tr>
    <tr>
      <th>5000</th>
      <td>2.619622</td>
      <td>0.005373</td>
      <td>0.001831</td>
    </tr>
    <tr>
      <th>10000</th>
      <td>7.042201</td>
      <td>0.006180</td>
      <td>0.002811</td>
    </tr>
    <tr>
      <th>20000</th>
      <td>21.625318</td>
      <td>0.008480</td>
      <td>0.005633</td>
    </tr>
  </tbody>
</table>
</div>


### 方法2と方法3をより大きなデータに対して検証する

方法2と方法3について、サンプルサイズを1000万まで増やして[^1]検証します。
大きさ1000万のデータでは、方法2は1.99秒ですが、方法3は2.31秒と方法2に対して0.31秒程度遅くなります。
一方で、サンプルサイズ1万のデータでは、逆に方法３の方が0.003秒程度速くなっているので、方法2は大きなデータを扱う場合に最適化されているようです。

```python
data = pd.concat(objs=[california_housing]*500, axis=0)
data.reset_index(drop=True, inplace=True)
data.shape
```

    (10320000, 8)

```python
from utilities.process_time import PandasProcessTimeMeasure

process_time_measure = PandasProcessTimeMeasure(
    data=data,
    sample_sizes=[100, 500, 1000, 5000, 10000, 50000, 100000, 500000, 1000000, 5000000, 10000000]
)
process_time_measure.set_method(name='method02', method=method2)
process_time_measure.set_method(name='method03', method=method3)
process_time_measure.measure_process_time_for_each_sample_sizes()
process_time_measure.plot_process_time()
```

![output_28_0.png](https://qiita-image-store.s3.amazonaws.com/0/218401/7600fa75-8158-db57-28ba-1dedd42a53ef.png)

```python
process_time_measure.process_time
```

<div>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>method02</th>
      <th>method03</th>
    </tr>
    <tr>
      <th>sample_size</th>
      <th></th>
      <th></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>100</th>
      <td>0.004440</td>
      <td>0.000823</td>
    </tr>
    <tr>
      <th>500</th>
      <td>0.004472</td>
      <td>0.000891</td>
    </tr>
    <tr>
      <th>1000</th>
      <td>0.004601</td>
      <td>0.000980</td>
    </tr>
    <tr>
      <th>5000</th>
      <td>0.005166</td>
      <td>0.001705</td>
    </tr>
    <tr>
      <th>10000</th>
      <td>0.005774</td>
      <td>0.002586</td>
    </tr>
    <tr>
      <th>50000</th>
      <td>0.013529</td>
      <td>0.012015</td>
    </tr>
    <tr>
      <th>100000</th>
      <td>0.023419</td>
      <td>0.023697</td>
    </tr>
    <tr>
      <th>500000</th>
      <td>0.101577</td>
      <td>0.116434</td>
    </tr>
    <tr>
      <th>1000000</th>
      <td>0.192417</td>
      <td>0.230766</td>
    </tr>
    <tr>
      <th>5000000</th>
      <td>1.030966</td>
      <td>1.229623</td>
    </tr>
    <tr>
      <th>10000000</th>
      <td>1.988032</td>
      <td>2.304879</td>
    </tr>
  </tbody>
</table>
</div>

## まとめ

ある1つのカラムを使って新たなカラムを追加する場合の方法について検証しました。

- 方法1　for文で新たな列を作る
- 方法2 DataFrameのスライシングで新たな列を作る
- 方法3 mapメソッドを使って新たな列を作る

その結果、次の3つについて分かりました。

1. 方法1は一番遅く、サンプルサイズの増加に対して、処理時間の増加幅が大きいので、大きなデータに対して使えない。
2. 方法2は、3つの中で最も速かった。
3. 方法3は、サンプルサイズ1000万のデータに対して、方法2より0.31秒程遅かった。

実際に使う場合は、方法3をおすすめします。
理由は、方法2とほぼ遜色ない速さで処理できて、可読性とメンテナンス性が方法2よりも高いからです。
方法2はどうしても処理能力を高めたい場合にのみ使うべきでしょう。


# 2. 複数のカラムで全ての組み合わせを持ったマスタを作成したい

すべての組み合わせを持ったマスタを作成する場合を検証します。
これが必要になるのは、例えば、ゲーム内のユーザー毎に各アイテムの所持数の推移を日次で見たい場合です。
ユーザーのアイテム購入数の擬似データを例にして考えていきます。
擬似データは作成スクリプトで生成し、csvファイルに`game_user_item.csv`で保存しています。
スクリプトは https://github.com/Katsuya-Ishiyama/pandas_tips/blob/master/generate_data_game_user_item.py にあります。

- user_id　ユーザーID
- date　日付
- item_id　アイテムID
- item_name　アイテム名
- item_purchase_count　該当日に該当するアイテムを購入した個数
- payment　該当日に該当するアイテムに課金した金額


```python
from itertools import product
from matplotlib import pyplot as plt
import numpy as np
import pandas as pd
from utilities.process_time import PandasProcessTimeMeasure

plt.rcParams['font.size'] = 12
plt.rcParams['figure.figsize'] = (10, 6)
```

```python
item_data = pd.read_csv('./data/game_user_item.csv')
item_data.head(5)
```

<div>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>user_id</th>
      <th>date</th>
      <th>item_id</th>
      <th>item_name</th>
      <th>item_purchase_count</th>
      <th>payment</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>0</th>
      <td>2855</td>
      <td>2018-04-14</td>
      <td>5</td>
      <td>item005</td>
      <td>0</td>
      <td>0</td>
    </tr>
    <tr>
      <th>1</th>
      <td>9634</td>
      <td>2018-05-23</td>
      <td>4</td>
      <td>item004</td>
      <td>4</td>
      <td>6000</td>
    </tr>
    <tr>
      <th>2</th>
      <td>5936</td>
      <td>2018-05-28</td>
      <td>10</td>
      <td>item010</td>
      <td>0</td>
      <td>0</td>
    </tr>
    <tr>
      <th>3</th>
      <td>6635</td>
      <td>2018-03-08</td>
      <td>9</td>
      <td>item009</td>
      <td>2</td>
      <td>3000</td>
    </tr>
    <tr>
      <th>4</th>
      <td>8864</td>
      <td>2018-05-27</td>
      <td>9</td>
      <td>item009</td>
      <td>1</td>
      <td>1500</td>
    </tr>
  </tbody>
</table>
</div>

各ユーザーともに日付が１日も欠けないように集計するためには、`item_data`からユーザー、日付、アイテムの全ての組み合わせを持ったマスター(ここでは`master`と呼ぶ)を作り、それに`item_data`を結合して、各ユーザー毎にアイテム獲得数の累積を計算していきます。
コードを例示すると、以下のようになります。

```python
master = some_method(...)       # 何らかの方法でマスター作成
tmp = master.merge(
    right=item_get,
    how='left',
    on=['user_id', 'data', 'item_id', 'item_name']
)
tmp.sort_values(['user_id', 'item_id', 'data'], inplace=True)
item_owned = tmp.groupby(['user_id', 'item_id']).item_purchase_count.rolling_sum(window=2)
```

`master`を作る処理は、全組み合わせを作成する必要があるため、かなり重たい処理になります。
ここを速度を気にせずに書いてしまうと、かなり遅い処理が出来上がってしまうので、どのような方法が良いのかを検討していきます。

```python
def create_values(sample_size):
    user_id_list = sorted(item_data.user_id.unique().tolist())
    date_list = sorted(item_data.date.unique().tolist())
    item_id_list = sorted(item_data.item_id.unique().tolist())
    item_name_list = sorted(item_data.item_name.unique().tolist())
    return user_id_list[:sample_size], date_list, item_id_list, item_name_list
```


## 方法1　1行毎にDataFrame化して`.append()`で積み上げる

まずは、作成した行を１回ずつDataFrameにしていく方法です。
初心者が書いてしまいがちなコードです。

```python
def method1(sample_size):
    user_id_list, date_list, item_id_list, item_name_list = create_values(sample_size)
    master = pd.DataFrame()
    for user_id in user_id_list:
        for date in date_list:
            for item_id, item_name in zip(item_id_list, item_name_list):
                _master = pd.DataFrame(
                    data={
                        'user_id': [user_id],
                        'date': [date],
                        'item_id': [item_id],
                        'item_name': [item_name]
                    },
                    columns=['user_id', 'date', 'item_id', 'item_name']
                )
                master = master.append(_master)
    master.reset_index(drop=True, inplace=True)
    return master


method1(10).head(10)
```

<div>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>user_id</th>
      <th>date</th>
      <th>item_id</th>
      <th>item_name</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>0</th>
      <td>1</td>
      <td>2018-01-01</td>
      <td>1</td>
      <td>item001</td>
    </tr>
    <tr>
      <th>1</th>
      <td>1</td>
      <td>2018-01-01</td>
      <td>2</td>
      <td>item002</td>
    </tr>
    <tr>
      <th>2</th>
      <td>1</td>
      <td>2018-01-01</td>
      <td>3</td>
      <td>item003</td>
    </tr>
    <tr>
      <th>3</th>
      <td>1</td>
      <td>2018-01-01</td>
      <td>4</td>
      <td>item004</td>
    </tr>
    <tr>
      <th>4</th>
      <td>1</td>
      <td>2018-01-01</td>
      <td>5</td>
      <td>item005</td>
    </tr>
    <tr>
      <th>5</th>
      <td>1</td>
      <td>2018-01-01</td>
      <td>6</td>
      <td>item006</td>
    </tr>
    <tr>
      <th>6</th>
      <td>1</td>
      <td>2018-01-01</td>
      <td>7</td>
      <td>item007</td>
    </tr>
    <tr>
      <th>7</th>
      <td>1</td>
      <td>2018-01-01</td>
      <td>8</td>
      <td>item008</td>
    </tr>
    <tr>
      <th>8</th>
      <td>1</td>
      <td>2018-01-01</td>
      <td>9</td>
      <td>item009</td>
    </tr>
    <tr>
      <th>9</th>
      <td>1</td>
      <td>2018-01-01</td>
      <td>10</td>
      <td>item010</td>
    </tr>
  </tbody>
</table>
</div>


## 方法2　listで全組み合わせ作成後にDataFrame化する　`.append()`バージョン

listにデータを吐き出しておいて、すべての組み合わせを作成後に一括してDataFrame化する方法です。
for文をいくつか重ねる必要がありますが、重たいDataFrameの操作を回避できるので、その分高速化が見込める方法です。

```python
def method2(sample_size):
        user_id_list, date_list, item_id_list, item_name_list = create_values(sample_size)
        master_user = []
        master_date = []
        master_item_id = []
        master_item_name = []
        for user_id in user_id_list:
            for date in date_list:
                for item_id, item_name in zip(item_id_list, item_name_list):
                    master_user.append(user_id)
                    master_date.append(date)
                    master_item_id.append(item_id)
                    master_item_name.append(item_name)
        master = pd.DataFrame(
            data={
                'user_id': master_user,
                'date': master_date,
                'item_id': master_item_id,
                'item_name': master_item_name
            },
            columns=['user_id', 'date', 'item_id', 'item_name']
        )
        return master


method2(10).head(10)
```

<div>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>user_id</th>
      <th>date</th>
      <th>item_id</th>
      <th>item_name</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>0</th>
      <td>1</td>
      <td>2018-01-01</td>
      <td>1</td>
      <td>item001</td>
    </tr>
    <tr>
      <th>1</th>
      <td>1</td>
      <td>2018-01-01</td>
      <td>2</td>
      <td>item002</td>
    </tr>
    <tr>
      <th>2</th>
      <td>1</td>
      <td>2018-01-01</td>
      <td>3</td>
      <td>item003</td>
    </tr>
    <tr>
      <th>3</th>
      <td>1</td>
      <td>2018-01-01</td>
      <td>4</td>
      <td>item004</td>
    </tr>
    <tr>
      <th>4</th>
      <td>1</td>
      <td>2018-01-01</td>
      <td>5</td>
      <td>item005</td>
    </tr>
    <tr>
      <th>5</th>
      <td>1</td>
      <td>2018-01-01</td>
      <td>6</td>
      <td>item006</td>
    </tr>
    <tr>
      <th>6</th>
      <td>1</td>
      <td>2018-01-01</td>
      <td>7</td>
      <td>item007</td>
    </tr>
    <tr>
      <th>7</th>
      <td>1</td>
      <td>2018-01-01</td>
      <td>8</td>
      <td>item008</td>
    </tr>
    <tr>
      <th>8</th>
      <td>1</td>
      <td>2018-01-01</td>
      <td>9</td>
      <td>item009</td>
    </tr>
    <tr>
      <th>9</th>
      <td>1</td>
      <td>2018-01-01</td>
      <td>10</td>
      <td>item010</td>
    </tr>
  </tbody>
</table>
</div>

## 方法3　listで全組み合わせ作成後にDataFrame化する　`.extend()`バージョン
こちらも方法2と同じようにすべての組み合わせを作成した後に、一括してDataFrame化する方法です。
方法2と異なる点は、`append`メソッドではなく`extend`メソッドを使っていることです。
このようにすることでfor文を1つ減らすことができ、その分、方法2よりも高速化が見込めます。
難点としては、方法2よりも分かりづらいコードになるため、間違いを起こしやすいです。

```python
def method3(sample_size):
        user_id_list, date_list, item_id_list, item_name_list = create_values(sample_size)
        master_user = []
        master_date = []
        master_item_id = []
        master_item_name = []
        n_item = len(item_id_list)
        for user_id in user_id_list:
            for date in date_list:
                master_user.extend([user_id] * n_item)
                master_date.extend([date] * n_item)
                master_item_id.extend(item_id_list)
                master_item_name.extend(item_name_list)
        master = pd.DataFrame(
            data={
                'user_id': master_user,
                'date': master_date,
                'item_id': master_item_id,
                'item_name': master_item_name
            },
            columns=['user_id', 'date', 'item_id', 'item_name']
        )
        return master


method3(10).head(10)
```

<div>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>user_id</th>
      <th>date</th>
      <th>item_id</th>
      <th>item_name</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>0</th>
      <td>1</td>
      <td>2018-01-01</td>
      <td>1</td>
      <td>item001</td>
    </tr>
    <tr>
      <th>1</th>
      <td>1</td>
      <td>2018-01-01</td>
      <td>2</td>
      <td>item002</td>
    </tr>
    <tr>
      <th>2</th>
      <td>1</td>
      <td>2018-01-01</td>
      <td>3</td>
      <td>item003</td>
    </tr>
    <tr>
      <th>3</th>
      <td>1</td>
      <td>2018-01-01</td>
      <td>4</td>
      <td>item004</td>
    </tr>
    <tr>
      <th>4</th>
      <td>1</td>
      <td>2018-01-01</td>
      <td>5</td>
      <td>item005</td>
    </tr>
    <tr>
      <th>5</th>
      <td>1</td>
      <td>2018-01-01</td>
      <td>6</td>
      <td>item006</td>
    </tr>
    <tr>
      <th>6</th>
      <td>1</td>
      <td>2018-01-01</td>
      <td>7</td>
      <td>item007</td>
    </tr>
    <tr>
      <th>7</th>
      <td>1</td>
      <td>2018-01-01</td>
      <td>8</td>
      <td>item008</td>
    </tr>
    <tr>
      <th>8</th>
      <td>1</td>
      <td>2018-01-01</td>
      <td>9</td>
      <td>item009</td>
    </tr>
    <tr>
      <th>9</th>
      <td>1</td>
      <td>2018-01-01</td>
      <td>10</td>
      <td>item010</td>
    </tr>
  </tbody>
</table>
</div>

## 検証結果
今回はユーザー数を変えることで総レコード数を変化させていきます。
ユーザー数と総レコード数の関係は下表の通りです。

|ユーザー数 |日数 |アイテム数 |総レコード数 |
|-------------:|-----:|------------:|-------------:|
|10 |365 |10 |36,500 | 
|20 |365 |10 |73,000 |
|30 |365 |10 |109,500 |
|50 |365 |10 |182,500 |
|100 |365 |10 |365,000 |
|500 |365 |10 |1,825,000 |
|1,000 |365 |10 |3,650,000 |
|5,000 |365 |10 |18,250,000 |
|10,000 |365 |10 |36,500,000 |

### ユーザー数を10, 20, 30と変化させて方法1〜3を比較
まずはユーザー数を10, 20, 30と変化させて方法1〜3を試します。
総レコード数はそれぞれ36,500、73,000、109,500となっています。
方法1は他の2つに比べてかなり遅く、ユーザー数30人で592秒（約10分）掛かっていることが分かります。
機械学習や統計を使った分析を行うサービスでユーザー数が30人しかいないことは、例え、アクティブユーザーのみに絞ったとしてもありえないでしょう。
どんなビジネスモデルなのかにも依ってユーザー数は変わりますが、それでも方法1を安易に使うことはできません。

```python
process_time_measure = PandasProcessTimeMeasure(
    sample_sizes=[10, 20, 30]
)
process_time_measure.set_method(name='method01', method=method1)
process_time_measure.set_method(name='method02', method=method2)
process_time_measure.set_method(name='method03', method=method3)
process_time_measure.measure_process_time_for_each_sample_sizes()
process_time_measure.plot_process_time()
```

![output_12_0.png](https://qiita-image-store.s3.amazonaws.com/0/218401/be43d55b-c39f-c47f-9872-431f7dd2c21f.png)

```python
process_time_measure.process_time
```

<div>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>method01</th>
      <th>method02</th>
      <th>method03</th>
    </tr>
    <tr>
      <th>sample_size</th>
      <th></th>
      <th></th>
      <th></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>10</th>
      <td>107.301365</td>
      <td>0.529076</td>
      <td>0.516198</td>
    </tr>
    <tr>
      <th>20</th>
      <td>298.265140</td>
      <td>0.532413</td>
      <td>0.549056</td>
    </tr>
    <tr>
      <th>30</th>
      <td>591.759437</td>
      <td>0.546038</td>
      <td>0.545157</td>
    </tr>
  </tbody>
</table>
</div>

### 方法2と方法3をユーザー数を10,000まで変化させて比較
次に方法2と方法3に絞って、ユーザー数を10、50、100、500、1,000、5,000、10,000と変化させて比較検証します。
ユーザー数10,000人（レコード数36,500,000）では、方法2は15.7秒に対し方法3は11.3秒と方法3が4.4秒程速くなっています。
逆に言えば、for文を1つ減らした結果、36,500,000行の処理時間の違いはわずか4.4秒しかなかった、とも言えます。
落としたforが処理していたデータ数に依るところが大きいので、例えばアイテム数が50, 100とある場合には、方法3の有効性が増します。
アイテムのような項目はその時々によって数が増減するものなので、数が増えても処理時間の増加が小さい方法を選ぶべきでしょう。

```python
process_time_measure = PandasProcessTimeMeasure(
    sample_sizes=[10, 50, 100, 500, 1000, 5000, 10000]
)
process_time_measure.set_method(name='method02', method=method2)
process_time_measure.set_method(name='method03', method=method3)
process_time_measure.measure_process_time_for_each_sample_sizes()
process_time_measure.plot_process_time()
```

![output_15_0.png](https://qiita-image-store.s3.amazonaws.com/0/218401/deee9513-5028-0de8-fa51-d8ebc75998eb.png)

```python
process_time_measure.process_time
```

<div>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>method02</th>
      <th>method03</th>
    </tr>
    <tr>
      <th>sample_size</th>
      <th></th>
      <th></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>10</th>
      <td>0.519822</td>
      <td>0.512880</td>
    </tr>
    <tr>
      <th>50</th>
      <td>0.572985</td>
      <td>0.549018</td>
    </tr>
    <tr>
      <th>100</th>
      <td>0.664457</td>
      <td>0.629464</td>
    </tr>
    <tr>
      <th>500</th>
      <td>1.275971</td>
      <td>1.038292</td>
    </tr>
    <tr>
      <th>1000</th>
      <td>1.991046</td>
      <td>1.598195</td>
    </tr>
    <tr>
      <th>5000</th>
      <td>8.112805</td>
      <td>5.750179</td>
    </tr>
    <tr>
      <th>10000</th>
      <td>15.682714</td>
      <td>11.285188</td>
    </tr>
  </tbody>
</table>
</div>

## まとめ
ゲーム内のアイテム所持数を例に、マスターテーブルを作る方法について3パターンを検証しました。
その結果

- 方法１は他の2つの方法に比べて比較にならないほど遅く、実用的ではない
- 方法2は２番目に速く、3650万行を処理した時でも方法3に比べて4.4秒しか遅くならない
- 方法3はこの中では1番速いが、`.extend()`の部分が分かりづらく、ミスしやすい

ことが分かりました。
`extend`メソッドの部分が分かりづらいものの、データがスケールした場合のことを考えると方法3を採用することをオススメします。

# 3. 複数の列を元に列を修正したい（新しい列を作りたい）

ゲーム内アイテム購入データでアイテムの単価が間違っていたので、課金額を修正したいとします。

```python
from itertools import product
from matplotlib import pyplot as plt
import numpy as np
import pandas as pd
from utilities.process_time import PandasProcessTimeMeasure

plt.rcParams['font.size'] = 12
plt.rcParams['figure.figsize'] = (10, 6)
```

```python
item_data = pd.read_csv('./data/game_user_item.csv')
item_data.head(5)
```

<div>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>user_id</th>
      <th>date</th>
      <th>item_id</th>
      <th>item_name</th>
      <th>item_purchase_count</th>
      <th>payment</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>0</th>
      <td>2855</td>
      <td>2018-04-14</td>
      <td>5</td>
      <td>item005</td>
      <td>0</td>
      <td>0</td>
    </tr>
    <tr>
      <th>1</th>
      <td>9634</td>
      <td>2018-05-23</td>
      <td>4</td>
      <td>item004</td>
      <td>4</td>
      <td>6000</td>
    </tr>
    <tr>
      <th>2</th>
      <td>5936</td>
      <td>2018-05-28</td>
      <td>10</td>
      <td>item010</td>
      <td>0</td>
      <td>0</td>
    </tr>
    <tr>
      <th>3</th>
      <td>6635</td>
      <td>2018-03-08</td>
      <td>9</td>
      <td>item009</td>
      <td>2</td>
      <td>3000</td>
    </tr>
    <tr>
      <th>4</th>
      <td>8864</td>
      <td>2018-05-27</td>
      <td>9</td>
      <td>item009</td>
      <td>1</td>
      <td>1500</td>
    </tr>
  </tbody>
</table>
</div>

間違っていたアイテムのIDと正しい単価は下記の通りです。

|間違っていたアイテムID |正しい単価 |
|:--------------------------|-------------:|
|4                         |360         |
|6                         |1000       |
|7                         |680         |

この情報を元に`item_data`の`payment`を修正する方法について検証します。
なお、新しい列を追加する場合でも、同じ方法で処理できます。

```python
CORRECT_PRICE_MASTER = {
    4: 360,
    6: 1000,
    7: 680
}

def calculate_correct_payment(item_id, purchase_count, old_payment):
    correct_price = CORRECT_PRICE_MASTER.get(item_id)
    if correct_price is None:
        return old_payment
    new_payment = correct_price * purchase_count
    return new_payment
```

## 方法1　for文と`iterrows`メソッドで値を修正していく

```python
def method1(data):
    _data = data.copy()
    new_payment_list = []
    for i, row in _data.iterrows():
        new_payment = calculate_correct_payment(
            item_id=row.item_id,
            purchase_count=row.item_purchase_count,
            old_payment=row.payment
        )
        new_payment_list.append(new_payment)
    _data.loc[:, 'payment'] = new_payment_list
    return _data

method1(item_data.iloc[:100, :]).head()
```

<div>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>user_id</th>
      <th>date</th>
      <th>item_id</th>
      <th>item_name</th>
      <th>item_purchase_count</th>
      <th>payment</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>0</th>
      <td>2855</td>
      <td>2018-04-14</td>
      <td>5</td>
      <td>item005</td>
      <td>0</td>
      <td>0</td>
    </tr>
    <tr>
      <th>1</th>
      <td>9634</td>
      <td>2018-05-23</td>
      <td>4</td>
      <td>item004</td>
      <td>4</td>
      <td>1440</td>
    </tr>
    <tr>
      <th>2</th>
      <td>5936</td>
      <td>2018-05-28</td>
      <td>10</td>
      <td>item010</td>
      <td>0</td>
      <td>0</td>
    </tr>
    <tr>
      <th>3</th>
      <td>6635</td>
      <td>2018-03-08</td>
      <td>9</td>
      <td>item009</td>
      <td>2</td>
      <td>3000</td>
    </tr>
    <tr>
      <th>4</th>
      <td>8864</td>
      <td>2018-05-27</td>
      <td>9</td>
      <td>item009</td>
      <td>1</td>
      <td>1500</td>
    </tr>
  </tbody>
</table>
</div>

## 方法2　`apply`メソッドを使う

```python
def method2(data):
    _data = data.copy()
    new_payment = _data.apply(
        func=lambda row: calculate_correct_payment(row.item_id, row.item_purchase_count, row.payment),
        axis=1
    )
    _data.loc[:, 'payment'] = new_payment
    return _data

method2(item_data.iloc[:100, :]).head()
```

<div>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>user_id</th>
      <th>date</th>
      <th>item_id</th>
      <th>item_name</th>
      <th>item_purchase_count</th>
      <th>payment</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>0</th>
      <td>2855</td>
      <td>2018-04-14</td>
      <td>5</td>
      <td>item005</td>
      <td>0</td>
      <td>0</td>
    </tr>
    <tr>
      <th>1</th>
      <td>9634</td>
      <td>2018-05-23</td>
      <td>4</td>
      <td>item004</td>
      <td>4</td>
      <td>1440</td>
    </tr>
    <tr>
      <th>2</th>
      <td>5936</td>
      <td>2018-05-28</td>
      <td>10</td>
      <td>item010</td>
      <td>0</td>
      <td>0</td>
    </tr>
    <tr>
      <th>3</th>
      <td>6635</td>
      <td>2018-03-08</td>
      <td>9</td>
      <td>item009</td>
      <td>2</td>
      <td>3000</td>
    </tr>
    <tr>
      <th>4</th>
      <td>8864</td>
      <td>2018-05-27</td>
      <td>9</td>
      <td>item009</td>
      <td>1</td>
      <td>1500</td>
    </tr>
  </tbody>
</table>
</div>

## 方法3　必要な列をlistで取り出してfor文で計算する

```python
def method3(data):
    _data = data.copy()
    item_id_list = _data.item_id.tolist()
    item_purchase_count_list = _data.item_purchase_count.tolist()
    payment_list = _data.payment.tolist()
    new_payment_list = []
    for item_id, purchase_count, old_payment in zip(item_id_list, item_purchase_count_list, payment_list):
        new_payment = calculate_correct_payment(item_id, purchase_count, old_payment)
        new_payment_list.append(new_payment)
    _data.loc[:, 'payment'] = new_payment_list
    return _data

method3(item_data.iloc[:100, :]).head()
```

<div>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>user_id</th>
      <th>date</th>
      <th>item_id</th>
      <th>item_name</th>
      <th>item_purchase_count</th>
      <th>payment</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>0</th>
      <td>2855</td>
      <td>2018-04-14</td>
      <td>5</td>
      <td>item005</td>
      <td>0</td>
      <td>0</td>
    </tr>
    <tr>
      <th>1</th>
      <td>9634</td>
      <td>2018-05-23</td>
      <td>4</td>
      <td>item004</td>
      <td>4</td>
      <td>1440</td>
    </tr>
    <tr>
      <th>2</th>
      <td>5936</td>
      <td>2018-05-28</td>
      <td>10</td>
      <td>item010</td>
      <td>0</td>
      <td>0</td>
    </tr>
    <tr>
      <th>3</th>
      <td>6635</td>
      <td>2018-03-08</td>
      <td>9</td>
      <td>item009</td>
      <td>2</td>
      <td>3000</td>
    </tr>
    <tr>
      <th>4</th>
      <td>8864</td>
      <td>2018-05-27</td>
      <td>9</td>
      <td>item009</td>
      <td>1</td>
      <td>1500</td>
    </tr>
  </tbody>
</table>
</div>

## 検証結果

### 方法1〜3を10万件までのデータで比較

まずは10万件までのデータで比較します。
方法1は5.9秒、方法2は2.4秒掛かっていますが、方法3は0.1秒未満で終わっています。
方法1と方法2が遅いのはどちらも内部でDataFrameの操作を行なっているためですが、処理時間に2倍程度の開きが出ている原因については、今後の課題にさせてください。

```python
process_time_measure = PandasProcessTimeMeasure(
    data=item_data,
    sample_sizes=[100, 500, 1000, 5000, 10000, 50000, 100000]
)
process_time_measure.set_method(name='method01', method=method1)
process_time_measure.set_method(name='method02', method=method2)
process_time_measure.set_method(name='method03', method=method3)
process_time_measure.measure_process_time_for_each_sample_sizes()
process_time_measure.plot_process_time()
```

![output_12_0.png](https://qiita-image-store.s3.amazonaws.com/0/218401/a4199b95-430d-6e12-ebab-cfb3ad7ce061.png)

```python
process_time_measure.process_time
```

<div>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>method01</th>
      <th>method02</th>
      <th>method03</th>
    </tr>
    <tr>
      <th>sample_size</th>
      <th></th>
      <th></th>
      <th></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>100</th>
      <td>0.006275</td>
      <td>0.002978</td>
      <td>0.000399</td>
    </tr>
    <tr>
      <th>500</th>
      <td>0.029832</td>
      <td>0.012977</td>
      <td>0.000561</td>
    </tr>
    <tr>
      <th>1000</th>
      <td>0.060400</td>
      <td>0.023462</td>
      <td>0.000726</td>
    </tr>
    <tr>
      <th>5000</th>
      <td>0.279075</td>
      <td>0.121582</td>
      <td>0.002264</td>
    </tr>
    <tr>
      <th>10000</th>
      <td>0.567415</td>
      <td>0.245067</td>
      <td>0.004303</td>
    </tr>
    <tr>
      <th>50000</th>
      <td>2.773393</td>
      <td>1.184637</td>
      <td>0.020269</td>
    </tr>
    <tr>
      <th>100000</th>
      <td>5.856177</td>
      <td>2.426971</td>
      <td>0.046708</td>
    </tr>
  </tbody>
</table>
</div>

### 方法3を1000万件のデータで検証する

方法3のみ1000万件のデータに適用して、実際の処理時間を検証します。
結果は4.1秒で完了することができました。
面倒でもlistに取り出しておくと、すぐに結果を得ることができ、無駄なアイドリングタイムを減らせます。

```python
process_time_measure = PandasProcessTimeMeasure(
    data=item_data,
    sample_sizes=[100, 500, 1000, 5000, 10000, 50000, 100000, 1000000, 10000000]
)
process_time_measure.set_method(name='method03', method=method3)
process_time_measure.measure_process_time_for_each_sample_sizes()
process_time_measure.plot_process_time()
```

![output_15_0.png](https://qiita-image-store.s3.amazonaws.com/0/218401/443846c1-fedd-13f7-264e-f8b4544e4af8.png)

```python
process_time_measure.process_time
```

<div>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>method03</th>
    </tr>
    <tr>
      <th>sample_size</th>
      <th></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>100</th>
      <td>0.000415</td>
    </tr>
    <tr>
      <th>500</th>
      <td>0.000577</td>
    </tr>
    <tr>
      <th>1000</th>
      <td>0.000769</td>
    </tr>
    <tr>
      <th>5000</th>
      <td>0.002512</td>
    </tr>
    <tr>
      <th>10000</th>
      <td>0.004136</td>
    </tr>
    <tr>
      <th>50000</th>
      <td>0.019558</td>
    </tr>
    <tr>
      <th>100000</th>
      <td>0.043555</td>
    </tr>
    <tr>
      <th>1000000</th>
      <td>0.405703</td>
    </tr>
    <tr>
      <th>10000000</th>
      <td>4.149219</td>
    </tr>
  </tbody>
</table>
</div>


## まとめ

ゲーム内アイテム購入データで誤った単価で課金額が計算されている場合を例にして、複数の列を元に既存の列を修正する方法について検証しました。
その結果、

- 方法1は3つの方法の中で1番遅い
- 方法2は２番目に速く、１番シンプルなコードで記述できる
- 方法3は1番速いが、必要な列をすべてlistに変換する手間が掛かる

が分かりました。
手間は掛かるものの、方法3は1,000万件のデータでも4.1秒で完了するため、繰り返し実行する可能性があるものは、この方法で実装すべきです。


# Appendix

パフォーマンス計測に使ったクラス`PandasProcessTimeMeasure`は、この記事のシミュレーションが簡単にできるように、私が実装したものです。
コードを下に示します。
また、このコードはGitHubで随時更新する予定です。
https://github.com/Katsuya-Ishiyama/pandas_tips/blob/master/utilities/process_time.py

```python:process_time.py
# -*- coding: utf-8 -*-

import logging
import sys
import timeit
from typing import Callable, List, Any
from matplotlib import pyplot as plt
from pandas import DataFrame


logger = logging.getLogger()
stdout_handler = logging.StreamHandler(sys.stdout)
logging.basicConfig(
    level=logging.INFO,
    handlers=[stdout_handler],
    format='[%(asctime)s] %(levelname)s %(filename)s:%(funcName)s:L%(lineno)d - %(message)s'
)


class PandasProcessTimeMeasure(object):

    def __init__(self, sample_sizes: List[int], data: DataFrame=None, number: int=10):
        self.data = data
        self.sample_sizes = sample_sizes
        self.number = number
        self.methods = {}
        self.process_time = None

    def set_method(self, name: str, method: Callable[[Any], DataFrame]):
        self.methods.setdefault(name, method)

    def measure_average_process_time(self, method: Callable[[Any], DataFrame], args: tuple=None, kwargs: dict=None) -> float:
        if (args is None) and (kwargs is None):
            def test_func():
                method()
        elif (args is not None) and (kwargs is None):
            def test_func():
                method(*args)
        elif (args is None) and (kwargs is not None):
            def test_func():
                method(**kwargs)
        else:
            def test_func():
                method(*args, **kwargs)

        _number = self.number
        logger.debug('number of iterations at timeit: {}'.format(_number))
        logger.debug('start timeit')
        total_process_time_sec = timeit.timeit('test_func()', globals=locals(), number=_number)
        logger.debug('end timeit')
        average_process_time_sec = total_process_time_sec / float(_number)
        logger.debug('average process time: {} [sec]'.format(average_process_time_sec))
        return average_process_time_sec

    def create_sample_data(self, n: int) -> DataFrame:
        return self.data.sample(n)

    def measure_process_time_for_each_sample_sizes(self) -> DataFrame:
        _measurement_time = {'sample_size': self.sample_sizes}
        logger.debug('sample sizes: {}'.format(self.sample_sizes))
        for method_name, method in self.methods.items():
            logger.debug('processing method: {}'.format(method_name))
            average_process_times = []
            for n in self.sample_sizes:
                logger.debug('loop method_name = {}, sample_size = {}'.format(method_name, n))
                if self.data is not None:
                    logger.debug('data is supplied')
                    data = self.create_sample_data(n=n)
                    logger.debug('shape of data: {}, {}'.format(*data.shape))
                    _time = self.measure_average_process_time(method=method, args=(data,))
                else:
                    logger.debug('data is not supplied')
                    _time = self.measure_average_process_time(method=method, args=(n,))
                logger.debug('processing time: {} [sec]'.format(_time))
                average_process_times.append(_time)
            _measurement_time.setdefault(method_name, average_process_times)
            logger.debug('_measurement_time: {}'.format(_measurement_time))
        process_time = DataFrame(data=_measurement_time)
        process_time.set_index('sample_size', inplace=True)
        process_time.sort_index(axis=1, inplace=True)
        self.process_time = process_time
    
    def plot_process_time(self):
        self.process_time.plot()
        plt.xlabel('Sample Size')
        plt.ylabel('Processing Time [sec]')
        plt.show()
```


[^1]: 無理やりデータをかさ増ししています。。。後日、ゲームの例で出した疑似データに変更します。