概要
pandas.DataFrame
で数千行のデータの組み換えをやろうとしたときにかなり時間がかかったので、簡単な例で実行時間を確認してみた。
結論から言うと、他の様々なサイトで言及されているように、「行単位の追加はかなり時間がかかるが、列単位の追加は圧倒的に早い」ということになる。また、先にリストなどでデータを構成しておいてからDataFrameを生成する方法も高速なことが分かった。
問題設定
次のように、3つの列を持つ行データを1万個、DataFrame
に追加していく例を考える。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import time import numpy as np import pandas as pd col_list = ['one', 'two', 'three'] row_to_add = [1, 2, 3] start_time = time.time() ・・・それぞれの処理・・・ end_time = time.time() print("erapse time {} sec".format(end_time - start_time)) |
appendメソッド
append
メソッドは2つのDataFrame
を結合するメソッドで、行の追加方法としてもよく紹介されている。実行結果は以下の通りで約7秒(3回繰り返して同程度)。
1 2 3 4 5 6 |
df = pd.DataFrame(columns=col_list) for i in range(10000): df_to_add = pd.DataFrame([row_to_add], columns=col_list) df = df.append(df_to_add) # erapse time 6.999355792999268 sec |
append
でリストをDataFrame
にする際、リストをそのまま渡すと列と解釈されるので、2次元化して行であることを明示している。また列名を指定しないと新たな列として4~6列目に行が加えられていくので、加えるDataFrame
でも列名を指定している。
リストをそのまま渡して列として生成し、行インデックスに列名を渡してDataFrame
を生成してから'.T'
で転置している例なども見られた。
なお、この場合のDataFrameの各要素は整数型となる。
1 2 3 4 5 6 7 8 9 10 11 12 |
one two three 0 1 2 3 1 1 2 3 2 1 2 3 3 1 2 3 4 1 2 3 ... .. .. ... 9995 1 2 3 9996 1 2 3 9997 1 2 3 9998 1 2 3 9999 1 2 3 |
locプロパティーはインデックス指定に注意
DataFrame
のloc
プロパティーは、スライスによって複数行・列の要素の参照・代入ができる。これを利用して、空のDataFrame
に1行ずつ追加していく。実行時間は7秒台。
この場合のDataFrame
の各要素も整数になる。
1 2 3 4 5 |
df = pd.DataFrame(columns=col_list) for i in range(10000): df.loc[i, :] = row_to_add # erapse time 7.583117246627808 sec |
興味深いのことに、loc[i:, ]
ではなくてloc[i]
で指定すると実行時間が倍以上、20秒近くになる。
1 2 3 4 5 |
df = pd.DataFrame(columns=col_list) for i in range(10000): df.loc[i] = row_to_add # erapse time 19.00727939605713 sec |
なお、loc
の代わりにiloc
を使うと"IndexError: iloc cannot enlarge its target object"
とエラーになる。
DataFrameの領域を確保した場合
リストで確保した場合
予めデータのサイズがわかっている場合に、ダミーデータで埋めたリストで領域を確保してみる。領域を一気に確保して値を入れていくだけなので実行速度は速い。実行時間は0.7秒程度で、appendやlocで1行ずつ追加していくのに比べて1/10。
1 2 3 4 5 |
df = pd.DataFrame([[0] * 3] * 10000, columns=col_list) for i in range(10000): df.loc[i, :] = row_to_add # erapse time 0.6972208023071289 sec |
ここでloc[i, :]
をloc[i]
とすると、実行時間は0.5秒程度と少し早くなる。これは1行ずつ追加する場合と逆の傾向だが、この場合はその差は追加の場合に比べて小さい。
なお、この方法では領域が既に確保されているのでiloc
に変更しても同じ結果となる。
ndarrayで確保した場合
リストではなくndarrayで領域を確保してみると、実行速度はリストの場合と同程度。
1 2 3 4 5 |
df = pd.DataFrame(np.empty((10000, 3)), columns=col_list) for i in range(10000): df.loc[i, :] = row_to_add # erapse time 0.6625535488128662 sec |
ただし、この場合各要素は実数となる。整数が必要ならndarrayのコンストラクターでdtype='int'
を指定する。
1 2 3 4 5 6 7 8 9 10 11 12 |
one two three 0 1.0 2.0 3.0 1 1.0 2.0 3.0 2 1.0 2.0 3.0 3 1.0 2.0 3.0 4 1.0 2.0 3.0 ... ... ... ... 9995 1.0 2.0 3.0 9996 1.0 2.0 3.0 9997 1.0 2.0 3.0 9998 1.0 2.0 3.0 9999 1.0 2.0 3.0 |
ここでndarrayのdtype
を整数で指定すると実行時間が以下のような傾向となった。
- int8, int16→4秒台
- int32, int64→0.6秒台
ワード境界の中に値を埋め込んでいくのに時間がかかっていると考えられる。
列ごとのリストを加える方法はかなり速い
列ごとの辞書でDataFrameを生成する方法
列ごとのリストを作っておいて、それらから全体のデータを辞書として準備し、DataFrame
を生成する方法。
これは更に速く、実行時間は0.015秒前後。loc[i, :]
で行ごとに加えていく方法の1/1000の時間で済むことになる。
1 2 3 4 5 6 7 8 9 10 11 12 |
one = [] two = [] three = [] for i in range(10000): one += [1] two += [2] three += [3] all = [one, two, three] df = pd.DataFrame( data={'one':one, 'two':two, 'three':three}, columns=col_list) # erapse time 0.014957666397094727 sec |
ただし辞書のキーで列名を指定するところがやや煩雑か。
列単位でリストを加えていく方法
列ごとのリストを、順次DataFrame
に加えていく方法。
この場合もかなり速いが、上の方法では実行時間が一定しているのに対して、こちらは0.015~0.03秒と少しばらついて、ほんの僅かだが遅め。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
one = [] two = [] three = [] for i in range(10000): one += [1] two += [2] three += [3] df = pd.DataFrame(columns=col_list) df['one'] = one df['two'] = two df['three'] = three # erapse time 0.020946025848388672 sec |
列ごとのndarrayを加える方法
空のndarrayを準備して要素を加えていき、これを列単位でDataFrameに加える方法。
実行時間は0.25秒程度でリストの時の10倍の時間がかかっている。別途ndarrayの要素追加時部分だけの時間を計測すると、この部分だけで0.2秒台で、配列の要素追加のところで時間がかかっている。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
one = np.empty(0, dtype=int) two = np.empty(0) three = np.empty(0) for i in range(10000): one = np.append(one, [1]) two = np.append(two, [2]) three = np.append(three, [3]) df = pd.DataFrame(columns=col_list) df['one'] = one df['two'] = two df['three'] = three end_time = time.time() # erapse time 0.24968528747558594 sec |
2次元リストから生成する方法がベスト
リストなどを列ごとに加えるのではなく、2次元のリストを構成しておいて、それを使ってDataFrame
を生成する方法。
実行時間は0.01~0.02秒程度で、最も早い部類に入る。順次行を追加するという発想にコードも近く、速度・可読性ともに最適のようである。
1 2 3 4 5 6 |
list_data = [] for i in range(10000): list_data.append(row_to_add) df = pd.DataFrame(list_data, columns=col_list) # erapse time 0.011972188949584961 sec |
既存のDataFrameに追加する場合
既にデータがあるDataFrame
に新たな行を追加する場合を考える。これまでの例で、どうやらリストの形で操作するのが速そうで、DataFrame
からリストへの変換がそれなりに速いのなら、その方法が最もよさそうだと予想できる。
以下のコードはこのことを確認したもの。3つのパートに分かれていて、最初がこれまでと同じDataFrame
の生成、次がDataFrame
からリストへの変換、最後がリストへの追加と追加後のDataFrame
の生成となっている。
DataFrame
からリストへの変換は、to_numpy()
メソッドでndarray
が得られるので(DataFrame
のvalues
でもndarrayは非推奨)、それをtolist()
メソッドでリストに変換している。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
import time import pandas as pd start_time = time.time() # First, prepare the original dataset col_list = ('one', 'two', 'three') list_data = [] initial_row = [1, 2, 3] for i in range(10000): list_data.append(initial_row) df = pd.DataFrame(list_data, columns=col_list) time_to_create = time.time() print(df) # Second, prepare the list to add new data list_data = df.to_numpy().tolist() time_to_prepare_list = time.time() # Finally, add new data to list and generate the new DataFrame row_to_add = [10, 20, 30] for i in range(10000): list_data.append(row_to_add) df = pd.DataFrame(list_data, columns=col_list) time_to_append = time.time() print(df) print("time to create :{} sec".format(time_to_create - start_time)) print("time to prepare:{} sec".format(time_to_prepare_list - time_to_create)) print("time to append :{} sec".format(time_to_append - time_to_prepare_list)) # time to create :0.0070116519927978516 sec # time to prepare:0.006953001022338867 sec # time to append :0.012996912002563477 sec |
結果はかなり高速で、DataFrame
のままでloc
で追加するよりもはるかに速い。
まとめ
今回のケースの場合、1万行の追加でlocを使うと7秒で1行当たり0.0007秒。100行まとめて追加すると0.07秒で、この時点でリストに変換して追加した方が速くなる。
数少ない行を低頻度で追加するのでなければ、DataFrame
にまとまった行を追加したり、既にあるDataFrame
の構造を変換するには、一旦リストに変換してからデータを追加し、DataFrame
に変換し直した方が速いと言える。