Улучшение производительности группы панд

У меня есть приложение для машинного обучения, написанное на Python, которое включает в себя этап обработки данных. Когда я это написал, я сначала обработал данные на Pandas DataFrames, но когда это привело к ужасающей производительности, я в конце концов переписал ее с использованием vanilla Python, причем для циклов вместо векторизованных операций и списков и dicts вместо DataFrames и Series. К моему удивлению, производительность кода, написанного на vanilla Python, оказалась намного выше, чем у кода, написанного с помощью Pandas.

Поскольку мой код обработки рукописного кода значительно больше и более беспорядочен, чем исходный код Pandas, я не совсем отказался от использования Pandas, и сейчас я пытаюсь оптимизировать код Pandas без особого успех.

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

Я профилировал свой код с помощью Jupyter Notebook lprun, и основная часть времени тратится на следующие и другие похожие строки:

grouped_data = data.groupby('pk')
data[[v + 'Diff' for v in val_cols]] = grouped_data[val_cols].transform(lambda x: x - x.shift(1)).fillna(0)
data[[v + 'Mean' for v in val_cols]] = grouped_data[val_cols].rolling(4).mean().shift(1).reset_index()[val_cols]
(...)

... сочетание векторизованной и не-векторной обработки. Я понимаю, что не-векторизованные операции не будут быстрее, чем мои рукописные для циклов, так как это в основном то, что они находятся под капотом, но как они могут быть настолько медленнее? Мы говорим об ухудшении производительности 10-20x между моим рукописным кодом и кодом Pandas.

Я делаю что-то очень, очень неправильно?

Ответ 1

Нет, я не думаю, что ты должен отказываться от панд. Там определенно лучшие способы сделать то, что вы пытаетесь. Хитрость заключается в том, чтобы избегать apply/transform в любой форме, насколько это возможно. Избегайте их, как чумы. Они в основном реализованы как для циклов, так что вы также можете напрямую использовать циклы python for, которые работают на скорости C и обеспечивают более высокую производительность.

Реальное увеличение скорости - это то, где вы избавляетесь от циклов и используете функции панд, которые неявно векторизуют их операции. Например, ваша первая строка кода может быть значительно упрощена, как я скоро вам покажу.

В этом посте я описываю процесс настройки, а затем, для каждой строки в вашем вопросе, предлагаю улучшение, а также сравнение времени и правильности.

Настройка

data = {'pk' : np.random.choice(10, 1000)} 
data.update({'Val{}'.format(i) : np.random.randn(1000) for i in range(100)})

df = pd.DataFrame(data)
g = df.groupby('pk')
c = ['Val{}'.format(i) for i in range(100)]

transform + sub + shiftdiff

Ваша первая строка кода может быть заменена простым оператором diff:

v1 = df.groupby('pk')[c].diff().fillna(0)

Проверка работоспособности

v2 = df.groupby('pk')[c].transform(lambda x: x - x.shift(1)).fillna(0)

np.allclose(v1, v2)
True

Performance

Performance
%timeit df.groupby('pk')[c].transform(lambda x: x - x.shift(1)).fillna(0)
10 loops, best of 3: 44.3 ms per loop

%timeit df.groupby('pk')[c].diff(-1).fillna(0)
100 loops, best of 3: 9.63 ms per loop

Удаление избыточных операций индексирования

Что касается вашей второй строки кода, я не вижу особых возможностей для улучшения, хотя вы можете избавиться от вызова reset_index() + [val_cols], если ваш оператор groupby не рассматривает pk в качестве индекса :

g = df.groupby('pk', as_index=False)

Затем вторая строка кода сокращается до:

v3 = g[c].rolling(4).mean().shift(1)

Проверка работоспособности

g2 = df.groupby('pk')
v4 = g2[c].rolling(4).mean().shift(1).reset_index()[c]

np.allclose(v3.fillna(0), v4.fillna(0))
True

Performance

Performance
%timeit df.groupby('pk')[c].rolling(4).mean().shift(1).reset_index()[c]
10 loops, best of 3: 46.5 ms per loop

%timeit df.groupby('pk', as_index=False)[c].rolling(4).mean().shift(1)
10 loops, best of 3: 41.7 ms per loop

Обратите внимание, что время на разных компьютерах различается, поэтому обязательно тщательно протестируйте свой код, чтобы убедиться, что ваши данные действительно улучшаются.

Хотя в этот раз разница не так велика, вы можете оценить тот факт, что есть улучшения, которые вы можете сделать! Это может оказать гораздо большее влияние на большие данные.


Послесловие

В заключение, большинство операций выполняются медленно, поскольку их можно ускорить. Ключ должен избавиться от любого подхода, который не использует векторизацию.

Для этого иногда полезно выйти из пространства панд и ступить в пустышку. Операции над массивами numpy или использование numpy, как правило, выполняются намного быстрее, чем эквиваленты панд (например, np.sum быстрее, чем pd.DataFrame.sum, а np.where быстрее, чем pd.DataFrame.where и т.д.).

Иногда петель нельзя избежать. В этом случае вы можете создать базовую функцию зацикливания, которую затем можно векторизовать, используя numba или cython. Примеры этого можно найти здесь на Повышении производительности, прямо изо рта лошади.

В других случаях ваши данные слишком велики, чтобы их можно было разумно разместить в массивах. В этом случае пришло время сдаться и переключиться на dask или spark, которые предлагают высокопроизводительные распределенные вычислительные среды для работы с большими данными.