2015-07-29 5 views
10

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

В 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 любезно Зуй Нгуен:

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

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

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

+0

Интересный вопрос ... – Matt

+0

Для больших файлов csv загляните в парсер [pandas csv] (http://pandas.pydata.org/pandas-docs/version/0.13.1/generated/pandas.io.parsers .read_csv.html) с chunksize в качестве аргумента. – Parfait

+0

Я думал о 'read_csv' chunksize. Я профилировал свое фактическое приложение достаточно, чтобы увидеть, что синтаксический анализ не является узким местом. Файлы не такие «большие», всего около 60 тыс. Строк. Узким местом является создание большого блока данных. – andrew

ответ

18

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

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 
+0

Вы абсолютно правы. Я только что закончил бег, где я увеличил количество num_dfs до 300 сотен. В этом случае 'method3()' является явным победителем. Я напишу и протестирую 'method4()', который использовал 'pd.concat'. – andrew

4

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

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

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

+0

Я также считаю, что ваше решение делает preallocation более быстрым методом. Но я не принял в качестве ответа, потому что моя проблема требует использования differnt dtypes. Я отредактировал вопрос, чтобы отразить это. – andrew

4

@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 является 13x быстрее, чем предварительное выделение с np.empty(... dtype) palceholder.

1

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

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 это еще более значимо.

Смежные вопросы