Індикатор прогресу під час операцій панди


158

Я регулярно виконую операції панд на кадрах даних, що перевищують 15 мільйонів рядків, і я б хотів отримати доступ до показника прогресу для певних операцій.

Чи існує текстовий показник прогресу для операцій розділення-застосування-поєднання панд?

Наприклад, у чомусь подібному:

df_users.groupby(['userID', 'requestDate']).apply(feature_rollup)

де feature_rollupє дещо задіяна функція, яка займає багато стовпців DF та створює нові колонки користувачів за допомогою різних методів. Ці операції можуть зайняти деякий час для великих кадрів даних, тому я хотів би знати, чи можна вивести текстовий вихід у зошит iPython, який оновлює мене про прогрес.

Поки я намагався канонічні показники прогресу циклу для Python, але вони не взаємодіють з пандами жодним змістовно.

Я сподіваюся, що у бібліотеці / документації панд я помітив щось, що дозволяє зрозуміти хід комбінації розбиття-застосування. Проста реалізація, можливо, розгляне загальну кількість підмножин кадру даних, над якими applyфункціонує функція, і повідомить про хід як завершений дріб цих підмножин.

Це, можливо, щось, що потрібно додати до бібліотеки?


Ви зробили% код (профіль) на код? іноді ви можете робити операції на цілому кадрі, перш ніж подати заявку на усунення вузьких місць
Jeff

@Jeff: ти вважаєш, я це робив раніше, щоб витіснити з нього кожен останній шматочок. Проблема дійсно зводиться до межі зменшення псевдокартони, над якою я працюю, оскільки рядки складають десятки мільйонів, тому я не сподіваюсь на те, що надвисока швидкість збільшується, просто хочу трохи відгуку про прогрес.
cwharland

Подумайте про cythonising: pandas.pydata.org/pandas-docs/dev/…
Енді Хейден

@AndyHayden - Як я коментував вашу відповідь, ваша реалізація є досить хорошою і додає невелику кількість часу до загальної роботи. Я також цитонізував три операції в сукупності функцій, які відновлювали весь час, який зараз присвячується звіту про прогрес. Отже, врешті-решт, я думаю, що у мене з'являться панелі прогресу зі скороченням загального часу на обробку, якщо я дотримуюся цітону по всій функції.
cwharland

Відповіді:


277

Завдяки популярності попиту, 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).

EDIT


Щоб безпосередньо відповісти на початкове запитання, замініть:

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())

5
tqdmбув створений для оригінальних ітерабелів просто: Підтримка Pandas from tqdm import tqdm; for i in tqdm( range(int(1e8)) ): passбула нещодавним злом, який я зробив :)
casper.dcl

6
До речі, якщо ви використовуєте ноутбуки Юпітера, ви також можете використовувати tqdm_notebooks, щоб отримати гарнішу панель. Разом з пандами вам потрібно було б його інстанціювати, як from tqdm import tqdm_notebook; tqdm_notebook().pandas(*args, **kwargs) дивіться тут
grinsbaeckchen

2
З версії 4.8.1 - використовуйте замість tqdm.pandas (). github.com/tqdm/tqdm/commit/…
mork

1
Дякую, @mork правильно. Ми працюємо (повільно) у напрямку tqdmv5, що робить речі більш модульними.
casper.dcl

1
Нещодавні рекомендації щодо синтаксису дивіться у документації щодо tqdm Pandas тут: pypi.python.org/pypi/tqdm#pandas-integration
Manu CJ

18

Налаштувати відповідь Джеффа (і використовувати це як функцію повторного використання).

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

Примітка: застосувати оновлений відсоток оновлень в рядку . Якщо ваші функції відкладаються, це не працюватиме.

In [11]: g = df_users.groupby(['userID', 'requestDate'])

In [12]: f = feature_rollup

In [13]: logged_apply(g, f)
apply progress: 100%
Out[13]: 
...

Як завжди, ви можете додати це до своїх об'єктів у групі як метод:

from pandas.core.groupby import DataFrameGroupBy
DataFrameGroupBy.logged_apply = logged_apply

In [21]: g.logged_apply(f)
apply progress: 100%
Out[21]: 
...

Як зазначалося в коментарях, це не є особливістю, яку основні панди були б зацікавлені в реалізації. Але python дозволяє створити ці для багатьох об'єктів / методів панди (це зробило б досить багато роботи ... хоча ви повинні мати можливість узагальнити цей підхід).


Я кажу "зовсім трохи роботи", але ви, мабуть, могли б переписати всю цю функцію як (більш загального) декоратора.
Енді Хайден

Дякуємо за розширення посту Джеффа Я реалізував і те, і уповільнення для кожного є досить мінімальним (додав 1,1 хвилини до операції, на яку пішло 27 хвилин). Таким чином я можу побачити прогрес і враховуючи суттєвий характер цих операцій, я думаю, що це прийнятне уповільнення.
cwharland

Відмінно, радий, що це допомогло. Я насправді був здивований повільним сповільненням (коли я спробував приклад), я очікував, що це буде набагато гірше.
Енді Хайден

1
Щоб додатково додати ефективність опублікованих методів, я лінувався щодо імпорту даних (панди просто надто хороші в обробці безладного csv !!), і деякі мої записи (~ 1%) повністю вибиті з вставок (думаю, цілі записи, вставлені в окремі поля). Усунення цих причин призводить до значного прискорення збірки функцій, оскільки не було двозначності щодо того, що робити під час операцій роздільного застосування та поєднання.
cwharland

1
Я знижуюсь на 8 хвилин ... але я додав щось до групи функцій (більше можливостей -> кращий AUC!). Це 8 хвилин на шматок (зараз всього два шматки) з кожним шматочком в районі 12 мільйонів рядів. Так, так ... 16 хвилин, щоб зробити потужні операції на 24 мільйонах рядків, використовуючи HDFStore (а в складі функції немає nltk). Досить добре. Будемо сподіватися, що Інтернет не судить мене за початковою необізнаністю або амбівалентністю щодо заплутаних вставок =)
cwharland

11

Якщо вам потрібна підтримка того, як це використовувати в зошиті 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. Як згадується в статті, розвиток знаходиться в пізній бета-стадії.


8

Для всіх, хто хоче застосувати tqdm до власного коду паралельних панд - застосувати.

(Я намагався деякі бібліотеки проводити паралелізацію протягом багатьох років, але я ніколи не знаходив 100% -ного рішення паралелізації, головним чином для функції застосування, і мені завжди доводилося повертатися за своїм "ручним" кодом.)

df_multi_core - це той, кого ви телефонуєте. Він приймає:

  1. Ваш об'єкт df
  2. Назва функції, яку ви хочете зателефонувати
  3. Підмножина стовпців, над якими може виконуватися функція (сприяє скороченню часу / пам'яті)
  4. Кількість завдань, які потрібно виконати паралельно (-1 або опустіть для всіх ядер)
  5. Будь-які інші kwargs функції df приймає (наприклад, "вісь")

_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

Нижче наведено тестовий код для паралельного застосування з tqdm "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 рядок прогресу для запуску без паралелізації, а також основні смуги прогресу при запуску з паралелізацією. Є невеликий хик, і іноді решта ядер з’являються відразу, але навіть тоді я думаю, що це корисно, оскільки ви отримуєте статистику прогресу на ядро ​​(це / сек та загальна кількість записів, наприклад)

введіть тут опис зображення

Дякую @abcdaa за цю чудову бібліотеку!


1
Дякуємо @mork - не соромтесь додавати на github.com/tqdm/tqdm/wiki/How-to-make-a-great-Progress-Bar або створити нову сторінку на github.com/tqdm/tqdm/wiki
casper. dcl

Дякую, але довелося змінити цю частину: try: splits = np.array_split(df[subset], njobs) except ValueError: splits = np.array_split(df, njobs)через виняток KeyError замість ValueError змініть на Exception, щоб обробити всі випадки.
Маріус

Спасибі @mork - ця відповідь повинна бути вищою.
Енді

5

Ви можете легко зробити це за допомогою декоратора

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)

тоді просто скористайтеся функцією модифікованої_файли (та змініть, коли ви хочете, щоб вона друкувала)


1
Очевидне попередження: це призведе до уповільнення вашої функції! Ви можете навіть оновити його за допомогою прогресу stackoverflow.com/questions/5426546/…, наприклад, підрахунок / лень у відсотках.
Енді Хейден

так - у вас буде порядок (кількість груп), тож залежно від того, що це вузьке місце, це може змінити значення
Jeff

можливо, інтуїтивне, що потрібно зробити, це зафіксувати це у logged_apply(g, func)функції, де ви мали б доступ до замовлення та могли б увійти з самого початку.
Енді Хайден

Я зробив вище, у своїй відповіді, також нахабний відсоток оновлення. Насправді я не міг налагодити вашу роботу ... Я думаю, що з обгортанням. Якщо ви використовуєте його для застосування, це все одно не так важливо.
Енді Хейден

1

Я змінив відповідь Джеффа , щоб включити загальну суму, щоб ви могли відслідковувати хід і змінну, щоб просто надрукувати кожну 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, відповідь Енді Хайдена робить це без нього

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.