Создание больших Pandas DataFrames: preallocation vs append vs concat

Меня смущает производительность в Pandas при создании большого фрагмента блока данных куском. В Numpy мы (почти) всегда видим лучшую производительность, предварительно распределяя большой пустой массив и затем заполняя значения. Насколько я понимаю, это связано с тем, что Numpy захватывает всю память, которая ему нужна, вместо того, чтобы перераспределять память при каждой операции append.

В Pandas я, кажется, получаю лучшую производительность, используя шаблон df = df.append(temp).

Вот пример времени. Далее следует определение класса Timer. Как вы видите, я обнаружил, что preallocating примерно в 10 раз медленнее, чем при использовании append! Предварительное выделение кадра данных с np.empty значениями соответствующего dtype очень помогает, но метод append по-прежнему является самым быстрым.

import numpy as np
from numpy.random import rand
import pandas as pd

from timer import Timer

# Some constants
num_dfs = 10  # Number of random dataframes to generate
n_rows = 2500
n_cols = 40
n_reps = 100  # Number of repetitions for timing

# Generate a list of num_dfs dataframes of random values
df_list = [pd.DataFrame(rand(n_rows*n_cols).reshape((n_rows, n_cols)), columns=np.arange(n_cols)) for i in np.arange(num_dfs)]

##
# Define two methods of growing a large dataframe
##

# Method 1 - append dataframes
def method1():
    out_df1 = pd.DataFrame(columns=np.arange(4))
    for df in df_list:
        out_df1 = out_df1.append(df, ignore_index=True)
    return out_df1

def method2():
# # Create an empty dataframe that is big enough to hold all the dataframes in df_list
out_df2 = pd.DataFrame(columns=np.arange(n_cols), index=np.arange(num_dfs*n_rows))
#EDIT_1: Set the dtypes of each column
for ix, col in enumerate(out_df2.columns):
    out_df2[col] = out_df2[col].astype(df_list[0].dtypes[ix])
# Fill in the values
for ix, df in enumerate(df_list):
    out_df2.iloc[ix*n_rows:(ix+1)*n_rows, :] = df.values
return out_df2

# EDIT_2: 
# Method 3 - preallocate dataframe with np.empty data of appropriate type
def method3():
    # Create fake data array
    data = np.transpose(np.array([np.empty(n_rows*num_dfs, dtype=dt) for dt in df_list[0].dtypes]))
    # Create placeholder dataframe
    out_df3 = pd.DataFrame(data)
    # Fill in the real values
    for ix, df in enumerate(df_list):
        out_df3.iloc[ix*n_rows:(ix+1)*n_rows, :] = df.values
    return out_df3

##
# Time both methods
##

# Time Method 1
times_1 = np.empty(n_reps)
for i in np.arange(n_reps):
    with Timer() as t:
       df1 = method1()
    times_1[i] = t.secs
print 'Total time for %d repetitions of Method 1: %f [sec]' % (n_reps, np.sum(times_1))
print 'Best time: %f' % (np.min(times_1))
print 'Mean time: %f' % (np.mean(times_1))

#>>  Total time for 100 repetitions of Method 1: 2.928296 [sec]
#>>  Best time: 0.028532
#>>  Mean time: 0.029283

# Time Method 2
times_2 = np.empty(n_reps)
for i in np.arange(n_reps):
    with Timer() as t:
        df2 = method2()
    times_2[i] = t.secs
print 'Total time for %d repetitions of Method 2: %f [sec]' % (n_reps, np.sum(times_2))
print 'Best time: %f' % (np.min(times_2))
print 'Mean time: %f' % (np.mean(times_2))

#>>  Total time for 100 repetitions of Method 2: 32.143247 [sec]
#>>  Best time: 0.315075
#>>  Mean time: 0.321432

# Time Method 3
times_3 = np.empty(n_reps)
for i in np.arange(n_reps):
    with Timer() as t:
        df3 = method3()
    times_3[i] = t.secs
print 'Total time for %d repetitions of Method 3: %f [sec]' % (n_reps, np.sum(times_3))
print 'Best time: %f' % (np.min(times_3))
print 'Mean time: %f' % (np.mean(times_3))

#>>  Total time for 100 repetitions of Method 3: 6.577038 [sec]
#>>  Best time: 0.063437
#>>  Mean time: 0.065770

Я использую любезность Timer от Huy Nguyen:

# credit: http://www.huyng.com/posts/python-performance-analysis/

import time

class Timer(object):
    def __init__(self, verbose=False):
        self.verbose = verbose

    def __enter__(self):
        self.start = time.clock()
        return self

    def __exit__(self, *args):
        self.end = time.clock()
        self.secs = self.end - self.start
        self.msecs = self.secs * 1000  # millisecs
        if self.verbose:
            print 'elapsed time: %f ms' % self.msecs

Если вы все еще следуете, у меня есть два вопроса:

1) Почему метод append работает быстрее? (ПРИМЕЧАНИЕ: для очень маленьких кадров данных, т.е. n_rows = 40, это на самом деле медленнее).

2) Каков наиболее эффективный способ создания большого блока данных из кусков? (В моем случае куски - это большие файлы csv).

Спасибо за вашу помощь!

EDIT_1: В моем проекте реального мира столбцы имеют разные типы. Поэтому я не могу использовать трюк pd.DataFrame(.... dtype=some_type) для улучшения производительности preallocation, по рекомендации BrenBarn. Параметр dtype заставляет все столбцы быть одним и тем же типом [Ref. issue 4464]

Я добавил несколько строк в method2() в свой код, чтобы изменить столбцы по столбцам dtypes для соответствия во входных данных. Эта операция является дорогостоящей и отрицает преимущества наличия соответствующих типов при записи блоков строк.

EDIT_2: попробуйте предварительно распределить фрейм с использованием массива-заполнителя np.empty(... dtyp=some_type). Per @Joris предложение.

Ответ 1

Ваш тест на самом деле слишком мал, чтобы показать реальную разницу. Добавляя, копирует EACH time, поэтому вы на самом деле выполняете копирование размера N памяти N * (N-1) раз. Это ужасно неэффективно по мере роста размера вашего фреймворка. Это, конечно, может не иметь значения в очень маленькой рамке. Но если у вас есть реальные размеры, это очень важно. Это специально указано в документах здесь, хотя это небольшое предупреждение.

In [97]: df = DataFrame(np.random.randn(100000,20))

In [98]: df['B'] = 'foo'

In [99]: df['C'] = pd.Timestamp('20130101')

In [103]: df.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 100000 entries, 0 to 99999
Data columns (total 22 columns):
0     100000 non-null float64
1     100000 non-null float64
2     100000 non-null float64
3     100000 non-null float64
4     100000 non-null float64
5     100000 non-null float64
6     100000 non-null float64
7     100000 non-null float64
8     100000 non-null float64
9     100000 non-null float64
10    100000 non-null float64
11    100000 non-null float64
12    100000 non-null float64
13    100000 non-null float64
14    100000 non-null float64
15    100000 non-null float64
16    100000 non-null float64
17    100000 non-null float64
18    100000 non-null float64
19    100000 non-null float64
B     100000 non-null object
C     100000 non-null datetime64[ns]
dtypes: datetime64[ns](1), float64(20), object(1)
memory usage: 17.5+ MB

Добавление данных

In [85]: def f1():
   ....:     result = df
   ....:     for i in range(9):
   ....:         result = result.append(df)
   ....:     return result
   ....: 

Concat

In [86]: def f2():
   ....:     result = []
   ....:     for i in range(10):
   ....:         result.append(df)
   ....:     return pd.concat(result)
   ....: 

In [100]: f1().equals(f2())
Out[100]: True

In [101]: %timeit f1()
1 loops, best of 3: 1.66 s per loop

In [102]: %timeit f2()
1 loops, best of 3: 220 ms per loop

Обратите внимание, что я даже не буду пытаться предварительно выделить. Это несколько сложно, особенно потому, что вы имеете дело с несколькими типами (например, вы можете создать гигантский фрейм и просто .loc, и он будет работать). Но pd.concat просто мертв просто, работает надежно и быстро.

И время ваших размеров сверху

In [104]: df = DataFrame(np.random.randn(2500,40))

In [105]: %timeit f1()
10 loops, best of 3: 33.1 ms per loop

In [106]: %timeit f2()
100 loops, best of 3: 4.23 ms per loop

Ответ 2

Вы не указали какие-либо данные или тип для out_df2, поэтому у него есть dtype объекта. Это позволяет присваивать ему значения очень медленно. Укажите тип float64:

out_df2 = pd.DataFrame(columns=np.arange(n_cols), index=np.arange(num_dfs*n_rows), dtype=np.float64)

Вы увидите резкое ускорение. Когда я попробовал, method2 с этим изменением примерно в два раза быстрее, чем method1.

Ответ 3

@Jeff, pd.concat побеждает на милю! Я сравнил четвертый метод, используя pd.concat с num_dfs = 500. Результаты недвусмысленно:

Определение method4():

# Method 4 - us pd.concat on df_list
def method4():
return pd.concat(df_list, ignore_index=True)

Результаты профилирования, используя тот же Timer в моем исходном вопросе:

Total time for 100 repetitions of Method 1: 3679.334655 [sec]
Best time: 35.570036
Mean time: 36.793347
Total time for 100 repetitions of Method 2: 1569.917425 [sec]
Best time: 15.457102
Mean time: 15.699174
Total time for 100 repetitions of Method 3: 325.730455 [sec]
Best time: 3.192702
Mean time: 3.257305
Total time for 100 repetitions of Method 4: 25.448473 [sec]
Best time: 0.244309
Mean time: 0.254485

Метод pd.concat в 13 раз быстрее, чем предварительное распределение с помощью np.empty(... dtype).

Ответ 4

Ответ Джеффа правильный, но я нашел, что для моего типа данных другое решение работает лучше.

def df_(): 
    return pd.DataFrame(['foo']*np.random.randint(100)).transpose()

k = 100
frames = [df_() for x in range(0, k)]

def f1():
    result = frames[0]
    for i in range(k-1):
        result = result.append(frames[i+1])
    return result

def f2():  
    result = []
    for i in range(k):
        result.append(frames[i])
    return pd.concat(result)

def f3():
    result = []
    for i in range(k):
       result.append(frames[i])

    n = 2
    while len(result) > 1:
        _result = []
        for i in range(0, len(result), n):
            _result.append(pd.concat(result[i:i+n]))
        result = _result
    return result[0]

Мои dataframes - это одна строка и различной длины - нулевые записи должны иметь какое-то отношение к тому, почему f3() преуспевает.

In [33]: f1().equals(f2())
Out[33]: True

In [34]: f1().equals(f3())
Out[34]: True

In [35]: %timeit f1()
357 ms ± 192 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

In [36]: %timeit f2()
562 ms ± 68.2 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

In [37]: %timeit f3()
215 ms ± 58.6 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

Вышеприведенные результаты все еще для k = 100, но при больших k это еще более значимо.