<jupyter_start><jupyter_text>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　該当日に該当するアイテムに課金した金額<jupyter_code>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)
item_data = pd.read_csv('./data/game_user_item.csv')
item_data.head(5)<jupyter_output><empty_output><jupyter_text>各ユーザーともに日付が１日も欠けないように集計するためには、`item_data`からユーザー、日付、アイテムの全ての組み合わせを持ったマスター(ここでは`master`と呼ぶ)を作り、それに`item_data`を結合して、各ユーザー毎にアイテム獲得数の累積を計算していきます。  コードを例示すると、以下のようになります。```pythonmaster = 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`を作る処理は、全組み合わせを作成する必要があるため、一般的にはかなり重たい処理です。  ここを速度を気にせずに書いてしまうと、かなり遅い処理が出来上がってしまうので、どのような方法が良いのかを検討していきます。<jupyter_code>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<jupyter_output><empty_output><jupyter_text>方法1　1行毎にDataFrame化して`.append()`で積み上げるまずは、作成した行を１回ずつDataFrameにしていく方法です。  初心者が書いてしまいがちなコードです。<jupyter_code>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)<jupyter_output><empty_output><jupyter_text>方法2　listで全組み合わせ作成後にDataFrame化する　`.append()`バージョンlistにデータを吐き出しておいて、すべての組み合わせを作成後に一括してDataFrame化する方法です。  for文をいくつか重ねる必要がありますが、重たいDataFrameの操作を回避できるので、その分高速化が見込める方法です。<jupyter_code>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)<jupyter_output><empty_output><jupyter_text>方法3　listで全組み合わせ作成後にDataFrame化する　`.extend()`バージョンこちらも方法2と同じようにすべての組み合わせを作成した後に、一括してDataFrame化する方法です。  方法2と異なる点は、`append`メソッドではなく`extend`メソッドを使っていることです。  このようにすることでfor文を1つ減らすことができ、その分、方法2よりも高速化が見込めます。  難点としては、方法2よりも分かりづらいコードになるため、間違いを起こしやすいです。<jupyter_code>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)<jupyter_output><empty_output><jupyter_text>検証結果今回はユーザー数を変えることで総レコード数を変化させていきます。  ユーザー数と総レコード数の関係は下表の通りです。|ユーザー数 |日数 |アイテム数 |総レコード数 ||-------------:|-----:|------------:|-------------:||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を安易に使うことはできません。<jupyter_code>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()
process_time_measure.process_time<jupyter_output><empty_output><jupyter_text>方法2と方法3をユーザー数を10000まで変化させて比較次に方法2と方法3に絞って、ユーザー数を10, 50, 100, 500, 1000, 5000, 10000と変化させて比較検証します。  ユーザー数10000人（レコード数36,500,000）では、方法2は15.7秒に対し方法3は11.3秒と方法3が4.4秒程速くなっています。  逆に言えば、for文を1つ減らした結果、36,500,000行の処理時間の違いはわずか4.4秒しかなかった、とも言えます。  落としたforが処理していたデータ数に依るところが大きいので、例えばアイテム数が50, 100とある場合には、方法3の有効性が増します。  アイテムのような項目はその時々によって数が増減するものなので、数が増えても処理時間の増加が小さい方法を選ぶべきでしょう。<jupyter_code>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()
process_time_measure.process_time<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.11234515164459632, 'fraction_numerical': 0.04670368788267122, 'mean_word_length': 4.249626307922272, 'pattern_counts': {'":': 0, '<': 29, '<?xml version=': 0, '>': 29, 'https://': 1, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '7', 'dir': 'jupyter-structured-clean-dedup', 'id': '650780', 'license': 'MIT', 'n_tokens_mistral': 3630, 'n_tokens_neox': 3285, 'n_words': 311, 'path': 'pandas_tips_02.ipynb', 'repo_name': 'Katsuya-Ishiyama/pandas_tips', 'size': '7023'}