Индикатор прогресса во время операций с пандами
Я регулярно выполняю операции pandas в кадрах данных более 15 миллионов или около того, и мне бы хотелось получить доступ к индикатору прогресса для определенных операций.
Есть ли индикатор прогресса на основе текста для операций pandas split-apply-comb?
Например, что-то вроде:
df_users.groupby(['userID', 'requestDate']).apply(feature_rollup)
где feature_rollup
- это несколько задействованная функция, которая занимает много столбцов DF и создает новые столбцы пользователей различными способами. Эти операции могут занять некоторое время для больших кадров данных, поэтому я хотел бы знать, возможно ли иметь текстовый вывод в ноутбуке iPython, который обновляет меня в ходе выполнения.
До сих пор я пробовал индикаторы прогресса канонического цикла для Python, но они не взаимодействуют с pandas каким-либо значимым образом.
Я надеюсь что-то, что я упустил в библиотеке/документации pandas, которая позволяет узнать о прогрессе split-apply-comb. Простая реализация могла бы рассмотреть общее количество подмножеств фреймов данных, на которых работает функция apply
, и сообщать о прогрессе как завершенную долю этих подмножеств.
Возможно, это что-то, что нужно добавить в библиотеку?
Ответы
Ответ 1
Из-за tqdm
спроса в tqdm
добавлена поддержка pandas
. В отличие от других ответов, это не будет заметно замедлять панд - вот пример для DataFrameGroupBy.progress_apply
:
import pandas as pd
import numpy as np
from tqdm import tqdm
# from tqdm.auto import tqdm # for notebooks
df = pd.DataFrame(np.random.randint(0, int(1e8), (10000, 1000)))
# Create and register a new 'tqdm' instance with 'pandas'
# (can use tqdm_gui, optional kwargs, etc.)
tqdm.pandas()
# Now you can use 'progress_apply' instead of 'apply'
df.groupby(0).progress_apply(lambda x: x**2)
Если вам интересно, как это работает (и как изменить его для ваших собственных обратных вызовов), посмотрите примеры на github, полную документацию по pypi или импортируйте модуль и запустите help(tqdm)
.
РЕДАКТИРОВАТЬ
Чтобы напрямую ответить на исходный вопрос, замените:
df_users.groupby(['userID', 'requestDate']).apply(feature_rollup)
с:
from tqdm import tqdm
tqdm.pandas()
df_users.groupby(['userID', 'requestDate']).progress_apply(feature_rollup)
Примечание: tqdm <= v4.8: для версий tqdm ниже 4.8 вместо tqdm.pandas()
вам нужно было сделать:
from tqdm import tqdm, tqdm_pandas
tqdm_pandas(tqdm())
Ответ 2
Чтобы настроить ответ Джеффа (и использовать его как функцию повторного использования).
def logged_apply(g, func, *args, **kwargs):
step_percentage = 100. / len(g)
import sys
sys.stdout.write('apply progress: 0%')
sys.stdout.flush()
def logging_decorator(func):
def wrapper(*args, **kwargs):
progress = wrapper.count * step_percentage
sys.stdout.write('\033[D \033[D' * 4 + format(progress, '3.0f') + '%')
sys.stdout.flush()
wrapper.count += 1
return func(*args, **kwargs)
wrapper.count = 0
return wrapper
logged_func = logging_decorator(func)
res = g.apply(logged_func, *args, **kwargs)
sys.stdout.write('\033[D \033[D' * 4 + format(100., '3.0f') + '%' + '\n')
sys.stdout.flush()
return res
Примечание. Процент хода выполнения обновляет встроенный. Если ваша функция stdout, то это не сработает.
In [11]: g = df_users.groupby(['userID', 'requestDate'])
In [12]: f = feature_rollup
In [13]: logged_apply(g, f)
apply progress: 100%
Out[13]:
...
Как обычно, вы можете добавить это в свои объекты groupby как метод:
from pandas.core.groupby import DataFrameGroupBy
DataFrameGroupBy.logged_apply = logged_apply
In [21]: g.logged_apply(f)
apply progress: 100%
Out[21]:
...
Как уже упоминалось в комментариях, это не функция, которая будет интересна для ядра pandas. Но python позволяет создавать их для многих объектов и методов pandas (сделать это будет довольно немного работы... хотя вы должны иметь возможность обобщить этот подход).
Ответ 3
Если вам нужна поддержка, как использовать это в записной книжке Jupyter/ipython, как я это сделал, вот полезное руководство и источник для соответствующей статьи:
from tqdm._tqdm_notebook import tqdm_notebook
import pandas as pd
tqdm_notebook.pandas()
df = pd.DataFrame(np.random.randint(0, int(1e8), (10000, 1000)))
df.groupby(0).progress_apply(lambda x: x**2)
Обратите внимание на подчеркивание в операторе импорта для _tqdm_notebook
. Как упоминается в упомянутой статье, разработка находится на поздней стадии бета-тестирования.
Ответ 4
Вы можете легко сделать это с помощью декоратора
from functools import wraps
def logging_decorator(func):
@wraps
def wrapper(*args, **kwargs):
wrapper.count += 1
print "The function I modify has been called {0} times(s).".format(
wrapper.count)
func(*args, **kwargs)
wrapper.count = 0
return wrapper
modified_function = logging_decorator(feature_rollup)
то просто используйте функцию modified_function (и измените, когда вы хотите его распечатать)
Ответ 5
Для тех, кто хочет применить tqdm к своему собственному параллельному коду pandas-apply.
(В течение многих лет я пытался использовать некоторые библиотеки для распараллеливания, но я так и не нашел 100% -ное решение для распараллеливания, в основном для функции apply, и мне всегда приходилось возвращаться для своего "ручного" кода.)
df_multi_core - это тот, кого вы называете. Он принимает:
- Ваш объект DF
- Имя функции, которую вы хотите вызвать
- Подмножество столбцов, над которыми может выполняться функция (помогает сократить время/память)
- Количество заданий, выполняемых параллельно (-1 или пропущено для всех ядер)
- Любые другие kwargs, которые принимает функция df (например, "axis")
_df_split - это внутренняя вспомогательная функция, которая должна располагаться глобально к работающему модулю (Pool.map зависит от места размещения), в противном случае я бы обнаружил ее внутри.
вот код из моей сущности (я добавлю туда еще функциональные тесты панд):
import pandas as pd
import numpy as np
import multiprocessing
from functools import partial
def _df_split(tup_arg, **kwargs):
split_ind, df_split, df_f_name = tup_arg
return (split_ind, getattr(df_split, df_f_name)(**kwargs))
def df_multi_core(df, df_f_name, subset=None, njobs=-1, **kwargs):
if njobs == -1:
njobs = multiprocessing.cpu_count()
pool = multiprocessing.Pool(processes=njobs)
try:
splits = np.array_split(df[subset], njobs)
except ValueError:
splits = np.array_split(df, njobs)
pool_data = [(split_ind, df_split, df_f_name) for split_ind, df_split in enumerate(splits)]
results = pool.map(partial(_df_split, **kwargs), pool_data)
pool.close()
pool.join()
results = sorted(results, key=lambda x:x[0])
results = pd.concat([split[1] for split in results])
return results
Ниже приводится тестовый код для параллельного применения с tqtm "progress_apply".
from time import time
from tqdm import tqdm
tqdm.pandas()
if __name__ == '__main__':
sep = '-' * 50
# tqdm progress_apply test
def apply_f(row):
return row['c1'] + 0.1
N = 1000000
np.random.seed(0)
df = pd.DataFrame({'c1': np.arange(N), 'c2': np.arange(N)})
print('testing pandas apply on {}\n{}'.format(df.shape, sep))
t1 = time()
res = df.progress_apply(apply_f, axis=1)
t2 = time()
print('result random sample\n{}'.format(res.sample(n=3, random_state=0)))
print('time for native implementation {}\n{}'.format(round(t2 - t1, 2), sep))
t3 = time()
# res = df_multi_core(df=df, df_f_name='apply', subset=['c1'], njobs=-1, func=apply_f, axis=1)
res = df_multi_core(df=df, df_f_name='progress_apply', subset=['c1'], njobs=-1, func=apply_f, axis=1)
t4 = time()
print('result random sample\n{}'.format(res.sample(n=3, random_state=0)))
print('time for multi core implementation {}\n{}'.format(round(t4 - t3, 2), sep))
В выходных данных вы видите 1 индикатор выполнения для работы без распараллеливания, а также индикатор выполнения для каждого ядра при работе с распараллеливанием. Есть небольшой скачок, и иногда остальные ядра появляются сразу, но даже тогда я считаю его полезным, так как вы получаете статистику прогресса на ядро (it/sec и общее количество записей, например)
![enter image description here]()
Спасибо @abcdaa за эту замечательную библиотеку!
Ответ 6
Я изменил Jeff answer, чтобы включить общее количество, чтобы вы могли отслеживать прогресс и переменную, чтобы просто печатать каждые X итераций (это фактически улучшает производительность по большому счету, если "print_at" достаточно высок)
def count_wrapper(func,total, print_at):
def wrapper(*args):
wrapper.count += 1
if wrapper.count % wrapper.print_at == 0:
clear_output()
sys.stdout.write( "%d / %d"%(calc_time.count,calc_time.total) )
sys.stdout.flush()
return func(*args)
wrapper.count = 0
wrapper.total = total
wrapper.print_at = print_at
return wrapper
функция clear_output() находится от
from IPython.core.display import clear_output
Если не на IPython, то Andy Hayden отвечает без него