pandasにおけるデータの連続判定

第一章 序章

1.1 はじめに

 近年,機械学習やDeepLearning等のAIの分野の発展に伴ってプログラミング言語の中でもPythonの人気や需要は高まっている. その中でもPythonフレームワークであるpandasはAI等の学習における説明変数の整形やビックデータ等のデータ分析などで多々使われるツールである.
 しかし,プログラミング言語等々の初学者やソースコードを書くのに少し慣れてきた者にとっては便利であるがゆえに知識不足や経験不足から遠回りしやすく,データの大きいものを扱いがちな分野であるがゆえに処理速度が著しく低下したり,使用できるメモリを使い果たしてしまうことも考えられる.
 また,例え自ら調べたとしてもその書かれた記事等が数年前の記事であることも珍しくなく,一方でプログラミング言語等のバージョンは頻繁にアップデートされているため書かれている記事の内容を丸写しして実行してもうまくいかないことも残念ながらありうる.
 そこで,筆者が経験したことの中でサイト等記事になっていないことの中から"pandasにおけるデータの連続判定"について取り扱い,自分と同じように困っている人がいるかもしれないと思い記事にした.

1.2 目的

 Pythonでビックデータ等を扱う際,pandasというフレームワークを用いることでデータ分析を簡単に行うことが可能だ.しかし,ソースコードの如何によってはメモリ使用量を多く消費したり,処理速度が非常に遅くなってしまうケースが存在する.そのため処理速度を速くしたり,メモリ使用量を少なくしようと改善することはソースコードを書いている人にとってはそのソースコードの目的が何であれ共通の課題となりうる.その結果として,わからないこと等が出てきた際には調べるのが定石だが,調べていても自分の知りたいことが記事にされている保証はどこにもないのである.
 今回,筆者が上記のように陥った”pandasにおけるデータの連続判定”においては調べても”pandasにおけるデータの連続値のカウント”について述べている記事しか見つからなかった.連続判定と連続値のカウントは厳密に違うが,それらを応用することで目的のソースコードの記述はどちらであっても可能である.
 そこで”pandasにおけるデータの連続のカウント”の記事で記されていたソースコードと筆者が提案する”pandasにおけるデータの連続判定”を行うソースコードをメモリ使用量と処理速度の観点から複数のデータを用いて比較し,筆者が提案するソースコードの方が良いと考えられた場合,この記事と通してそのソースコードを勧めることで同じような内容で困っている方がこの記事を見つけて少しでも役に立つことが目的である.

 実際に”pandasにおけるデータの連続値のカウント”について”stackoverflow”や”Qiita”で書かれていたソースコードを以下に示す.

import pandas as pd

df = pd.DataFrame({'group': [1, 1, 2, 3, 3, 3, 3, 2, 2, 4, 4, 4],'value': [1, 2, 1, 1, 3, 2, 4, 2, 1, 3, 2, 1]})  

y = df['group']
df['new'] = y.groupby((y != y.shift()).cumsum()).cumcount() + 1

 ソースコードの1行目でpandasのインポートを行い,2行目に使用するDataframe型のデータを定義しており,実際に連続のカウントを行っているのは3行目以降である.  3行目のy = df['group']で対象にするDataframeのcolumnを設定することで4行目にcolumnが等しいグループにおいての”value”coulmnが連続している場合におけるカウントをdf変数の新しいcolumn”new”にカウント値を格納する.
 4行目の処理を詳しく説明すると,3行目で対象にしたDataframeのcolumnに対してgroupby()を行うことでgroupの値が等しいもの同士での処理が行われる.また,y != y.shift()で次の行の”group”の値が一致しているかを確認し,一致している行に対して累積和cumsum()を求め,それのカウントに1を足すcumcount()+1ことで連続値のカウントが可能である.

第二章 本論

 2.1 目的

今回の目的は”pandasにおけるデータの連続値のカウント”と検索した際に見られるソースコード

import pandas as pd

df = pd.DataFrame({'group': [1, 1, 2, 3, 3, 3, 3, 2, 2, 4, 4, 4],  'value': [1, 2, 1, 1, 3, 2, 4, 2, 1, 3, 2, 1]})
  
y = df['group']
df['new'] = y.groupby((y != y.shift()).cumsum()).cumcount() + 1

と筆者が提案するソースコードとの処理速度,メモリ使用量の検証.それに応じた各ソースコードのメリットデメリットについて考察することを目的とする.

 2.2 方法

 実験の実行環境は以下の通りである。

  • OS: Windows 10 Enterprise 64bit
  • プロセッサ:Intel Core i3-8130U 2.20 GHz
  • メモリ(RAM):4GB
  • ハードディスク:256GB SSD
  • Python 3.7.3
  • pandas 0.24.2

 2.3 結果

 2.3.0 準備

 用いるDataframe型のデータを

    repeat_time = 10  # 繰り返す回数
    dataframe_len = 10000  # Dataframeのindexの長さ

    for i in range(repeat_time):
        random.seed(i)
        group_list = [random.randint(0, 10) for j in range(dataframe_len)]
        random.seed(i+1)
        value_list = [random.randint(0, 10) for j in range(dataframe_len)]
        sample_df = pd.DataFrame({'group': group_list,
                                  'value': value_list})

 ランダムな整数を生成させたものをデータをした.

 2.3.1 処理速度の検証結果

 処理速度の観点において以下の連続値のカウントを行うソースコードを実行させた.

# 連続値のカウントを行うソースコード
import pandas as pd
import time
import random


def chk_dataframe_series(df):  # 比較するソースコード
    y = df['group']
    df['new'] = y.groupby((y != y.shift()).cumsum()).cumcount() + 1


if __name__ == '__main__':

    repeat_time = 10  # 繰り返す回数
    cum_execute_time = 0  # 累計処理時間
    dataframe_len = 10000  # Dataframeのindexの長さ

    for i in range(repeat_time):
        # データ作成
        random.seed(i)
        group_list = [random.randint(0, 10) for j in range(dataframe_len)]
        random.seed(i+1)
        value_list = [random.randint(0, 10) for j in range(dataframe_len)]
        sample_df = pd.DataFrame({'group': group_list,
                                  'value': value_list})

        start = time.time()  # 処理開始時間
        chk_dataframe_series(sample_df)
        end = time.time()  # 処理終了時間
        execute_time = end - start  # 1回の処理時間
        cum_execute_time += execute_time  # 累計処理時間
        print('one time runtime(', i+1, '):', execute_time)  # 1回の処理時間表示

    print('average runtime:', cum_execute_time/repeat_time)  # 10回の処理時間の平均表示

 上記の連続値のカウントを行うソースコードの実行結果は以下の通りであった. f:id:RandyGen:20210304155623p:plain

 一方同じ観点において,以下の筆者が提案するデータの連続判定を行うソースコードを実行させた.

# データの連続判定を行うソースコード
import pandas as pd
import time
import random


def chk_dataframe_series(df):  # 比較するソースコード
    sorted_df = df.drop_duplicates(subset=['group', 'value']).sort_values(['group', 'value'])
    created_df = sorted_df[(sorted_df.group != sorted_df.group.shift()) & (sorted_df.value.diff() != 1)]


if __name__ == '__main__':

    repeat_time = 10  # 繰り返す回数
    cum_execute_time = 0  # 累計処理時間
    dataframe_len = 10000  # Dataframeのindexの長さ

    for i in range(repeat_time):
        # データ作成
        random.seed(i)
        group_list = [random.randint(0, 10) for j in range(dataframe_len)]
        random.seed(i+1)
        value_list = [random.randint(0, 10) for j in range(dataframe_len)]
        sample_df = pd.DataFrame({'group': group_list,
                                  'value': value_list})

        start = time.time()  # 処理開始時間
        chk_dataframe_series(sample_df)
        end = time.time()  # 処理終了時間
        execute_time = end - start  # 1回の処理時間
        cum_execute_time += execute_time  # 累計処理時間
        print('one time runtime(', i+1, '):', execute_time)  # 1回の処理時間表示

    print('average runtime:', cum_execute_time/repeat_time)  # 10回の処理時間の平均表示

 上記のデータの連続判定を行うソースコードの実行結果は以下の通りであった. f:id:RandyGen:20210304155448p:plain

 また,上記の検証で用いた2×10000ではなくデータサイズによって処理速度が変化すると考えられることから,データサイズを2×100,2×1000,2×100000に変更して実行した際の実行結果を以下に示す.

 連続値のカウントを行うソースコード(2×100,2×1000) f:id:RandyGen:20210304160448p:plain f:id:RandyGen:20210304160521p:plain f:id:RandyGen:20210304175253p:plain

 データの連続判定を行うソースコード(2×100,2×1000) f:id:RandyGen:20210304160822p:plain f:id:RandyGen:20210304160617p:plain f:id:RandyGen:20210304175407p:plain

 2.3.1検証結果総括
データサイズ 2×100 2×1000 2×10000 2×100000
連続値カウント [sec] 0.00337 0.00429 0.00688 0.02927
連続判定 [sec] 0.00409 0.00474 0.00444 0.01019

 2.3.2 メモリ使用量の検証結果

また,メモリ使用量の観点において以下の連続値のカウントを行うソースコードを実行させた.

# 連続値のカウントを行うソースコード
import pandas as pd
import random
from memory_profiler import profile


@profile  # メモリ使用量検証用
def chk_dataframe_series(df):  # 比較するソースコード
    y = df['group']
    df['new'] = y.groupby((y != y.shift()).cumsum()).cumcount() + 1


if __name__ == '__main__':

    repeat_time = 10  # 繰り返す回数
    cum_execute_time = 0  # 累計処理時間
    dataframe_len = 1000  # Dataframeのindexの長さ

    # データ作成
    random.seed(0)
    group_list = [random.randint(0, 10) for j in range(dataframe_len)]
    random.seed(1)
    value_list = [random.randint(0, 10) for j in range(dataframe_len)]
    sample_df = pd.DataFrame({'group': group_list,
                              'value': value_list})

    chk_dataframe_series(sample_df)

 上記の連続値のカウントを行うソースコードの実行結果は以下の通りであった. f:id:RandyGen:20210304161841p:plain

 一方同じ観点において,以下の筆者が提案するデータの連続判定を行うソースコードを実行させた.

# データの連続判定を行うソースコード
import pandas as pd
import random
from memory_profiler import profile


@profile  # メモリ使用量検証用
def chk_dataframe_series(df):  # 比較するソースコード
    sorted_df = df.drop_duplicates(subset=['group', 'value']).sort_values(['group', 'value'])
    created_df = sorted_df[(sorted_df.group != sorted_df.group.shift()) & (sorted_df.value.diff() != 1)]


if __name__ == '__main__':

    repeat_time = 10  # 繰り返す回数
    cum_execute_time = 0  # 累計処理時間
    dataframe_len = 10000  # Dataframeのindexの長さ

    # データ作成
    random.seed(0)
    group_list = [random.randint(0, 10) for j in range(dataframe_len)]
    random.seed(1)
    value_list = [random.randint(0, 10) for j in range(dataframe_len)]
    sample_df = pd.DataFrame({'group': group_list,
                              'value': value_list})

    chk_dataframe_series(sample_df)

 上記のデータの連続判定を行うソースコードの実行結果は以下の通りであった. f:id:RandyGen:20210304162233p:plain

 2.3.2検証結果総括
全体 1行目 2行目
連続値カウント [MiB] 1.6 0.1 1.5
連続判定 [MiB] 0.5 0.5 0.0

 2.3.3 追加検証

 さらに,先ほど用いたデータと異なり同じグループとみなすcolumnが”group”のみではなく”group1,group2”と複数になったケースを考慮して以下の連続値のカウントを行うソースコードを実行させた.

# 連続値のカウントを行うソースコード
import pandas as pd
import random


def chk_dataframe_series(df):  # 比較するソースコード
    y = df['group1', 'group2']
    df['new'] = y.groupby((y != y.shift()).cumsum()).cumcount() + 1


if __name__ == '__main__':

    repeat_time = 10  # 繰り返す回数
    cum_execute_time = 0  # 累計処理時間
    dataframe_len = 100  # Dataframeのindexの長さ

    # データ作成
    random.seed(0)
    group1_list = [random.randint(0, 10) for j in range(dataframe_len)]
    random.seed(1)
    group2_list = [random.randint(0, 10) for j in range(dataframe_len)]
    random.seed(2)
    value_list = [random.randint(0, 10) for j in range(dataframe_len)]
    sample_df = pd.DataFrame({'group1': group1_list,
                              'group2': group2_list,
                              'value': value_list})

    chk_dataframe_series(sample_df)

    print('execute complete')

 上記の連続値のカウントを行うソースコードの実行結果は以下の通りであった. f:id:RandyGen:20210304165437p:plain

 一方同じ観点において,以下の筆者が提案するデータの連続判定を行うソースコードを実行させた.

# データの連続判定を行うソースコード
import pandas as pd
import random


def chk_dataframe_series(df):  # 比較するソースコード
    sorted_df = df.drop_duplicates(subset=['group1', 'group2', 'value']).sort_values(['group1', 'group2', 'value'])
    created_df = sorted_df[(sorted_df.group1 != sorted_df.group1.shift())
                           & (sorted_df.group2 != sorted_df.group2.shift())
                           & (sorted_df.value.diff() != 1)]


if __name__ == '__main__':

    repeat_time = 10  # 繰り返す回数
    cum_execute_time = 0  # 累計処理時間
    dataframe_len = 100  # Dataframeのindexの長さ

    # データ作成
    random.seed(0)
    group1_list = [random.randint(0, 10) for j in range(dataframe_len)]
    random.seed(1)
    group2_list = [random.randint(0, 10) for j in range(dataframe_len)]
    random.seed(2)
    value_list = [random.randint(0, 10) for j in range(dataframe_len)]
    sample_df = pd.DataFrame({'group1': group1_list,
                              'group2': group2_list,
                              'value': value_list})

    chk_dataframe_series(sample_df)

    print('execute complete')

  上記のデータの連続判定を行うソースコードの実行結果は以下の通りであった. f:id:RandyGen:20210304165711p:plain

 2.3.3検証結果総括
実行
連続値カウント 不可
連続判定

 2.4 考察

 2.4.1 2.3.1の検証結果の考察

 2.3.1の検証結果から2×10000のデータに対して連続値のカウントを行うソースコードの処理時間が0.00688 [sec],連続判定を行うソースコードの処理時間が0.00444 [sec]と連続判定を行うソースコードの方が処理時間が短かった.
 また,データサイズを2×100,2×1000,2×10000,2×100000と変更して処理を実行した結果,連続値のカウントを行うソースコードの処理時間が0.00337 [sec],0.00429 [sec],0.00688 [sec],0.02927 [sec]となったのに対して,連続判定を行うソースコードの処理時間は0.00409 [sec],0.00474 [sec],0.00444 [sec],0.01019 [sec]と連続値のカウントを行うソースコードの処理時間はデータサイズが大きさに比例して処理時間が長くなったが一方連続判定を行うソースコードの処理時間は連続値のカウントを行うソースコードの処理時間に比べ,データサイズの大きさに比例して長くなる処理時間の度合いが小さかった.
 これらのことから処理時間においてデータサイズが大きくなればなるほど処理時間の差は開くだろうと考えられる.

 2.4.2 2.3.2の検証結果の考察

 2.3.2の検証結果から2×10000のデータに対して連続値のカウントを行うソースコードのメモリ使用量が1.6 [MiB],連続判定を行うソースコードの処理時間が0.5 [MiB]と連続判定を行うソースコードの方がメモリ使用量が少なかった.
 また,各ソースコードにおいて連続値のカウントを行うソースコードではdf['new'] = y.groupby((y != y.shift()).cumsum()).cumcount() + 1, 連続判定を行うソースコードではsorted_df = df.drop_duplicates(subset=['group', 'value']).sort_values(['group', 'value'])の処理がほとんどのメモリ使用量を占めていた.
 今回の検証では連続判定を行うソースコードにおいて変数sorted_dfに対するメモリの解放を処理に加えなかったため,ソースコードdel sorted_dfを加えることでよりメモリ使用量を減少させることが可能であると考えられる.

 2.4.3 2.3.1と2.3.2の双方の検証結果に対する考察

 2.4.2の考察で各ソースコードにおいて連続値のカウントを行うソースコードではdf['new'] = y.groupby((y != y.shift()).cumsum()).cumcount() + 1, 連続判定を行うソースコードではsorted_df = df.drop_duplicates(subset=['group', 'value']).sort_values(['group', 'value'])の処理がほとんどのメモリ使用量を占めていたことから,2つのソースコードの処理時間の差もこれらのdf['new'] = y.groupby((y != y.shift()).cumsum()).cumcount() + 1sorted_df = df.drop_duplicates(subset=['group', 'value']).sort_values(['group', 'value'])が引き起こしているものだと考えられる.
 よって今回の2つのソースコードにおける処理速度とメモリ使用量の差はpandasのAPIであるsortdrop,またshiftcumsumcumcountのデータ応じた挙動を追求することでより厳密に解決できると考えられる.

 2.4.4 2.3.3追加検証の結果に対する考察

 2.3.3の検証結果から同じグループとみなすcolumnが”group”のみではなく”group1,group2”と複数になったデータを用いた場合,連続値のカウントを行うソースコードではKeyError: ('group1','group2')となり正常な処理が行われなかったのに対して,連続判定を行うソースコードではexecute completeと出力され正常に処理が行われたことが分かった.
 連続値のカウントを行うソースコードでエラーが発生した原因はdf['new'] = y.groupby((y != y.shift()).cumsum()).cumcount() + 1の処理においてy = df['group1', 'group2']と複数のcolumnにしてしまったことでyの型がpd.Seriesから変更されてしまいcumcountはpd.Seriesに対して処理を行うため結果的にKeyErrorとなってしまったと考えられる.
 このことから連続値のカウントを行うソースコードでは連続判定を行うソースコードに比べてgroupbyを用いていることから他への応用が利きにくい,柔軟性に欠けたソースコードなっていると考えられる.

 2.5 結論

 pandasにおける連続値のカウントを行う

import pandas as pd

df = pd.DataFrame({'group': [1, 1, 2, 3, 3, 3, 3, 2, 2, 4, 4, 4],
                              'value': [1, 2, 1, 1, 3, 2, 4, 2, 1, 3, 2, 1]})  
y = df['group']
df['new'] = y.groupby((y != y.shift()).cumsum()).cumcount() + 1

 上記のソースコードより以下のpandasにおける連続判定を行う

import pandas as pd

df = pd.DataFrame({'group': [1, 1, 2, 3, 3, 3, 3, 2, 2, 4, 4, 4],
                              'value': [1, 2, 1, 1, 3, 2, 4, 2, 1, 3, 2, 1]})  

sorted_df = df.drop_duplicates(subset=['group', 'value']).sort_values(['group', 'value'])
created_df = sorted_df[(sorted_df.group != sorted_df.group.shift()) & (sorted_df.value.diff() != 1)]

 このソースコードの方が処理速度,メモリ使用量,応用の利く柔軟性があるという点において優れている.

 2.6 文献

https://qiita.com/Masutani/items/3cea640da7d1f5f58af1

  • pandasにおけるデータの連続値のカウントのソースコード(stackoverflow)

https://stackoverflow.com/questions/27626542/counting-consecutive-positive-value-in-python-array

  • pandasの各API(shift,cumsum,cumcount)

https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.shift.html

https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.cumsum.html

https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.core.groupby.GroupBy.cumcount.html

 2.7 資料

 今回使用したソースコードGithubにて公開しています.

https://github.com/RandyGen/pandasVerification/tree/main/pandasVerification