Python и Pandas: делаем быстрее

Давно в блоге не было материалов для любителей Python. В прошлом году я провёл эксперимент: предложил студентам усовершенствовать свои фрагменты кода для предобработки данных. В некоторых местах я специально писал неоптимально, а в некоторых думал, что оптимально… сейчас расскажу, что из этого получилось. При чтении старайтесь не пролистывать быстро вниз: попробуйте догадаться, какие из предложенных вариантов кода самые быстрые.

bystro.jpg

Задача 1 — устранить знак доллара

Допустим, у нас есть дата-фрем Pandas, в котором один из столбцов строковый, в нём указана сумма и в конце стоит знак доллара, см. признак price в табл. Надо преобразовать его в целочисленный, т.е. убрать знак доллара и перевести тип в  int (см. признак price($)_v1).

pic1.png

Возможны следующие варианты решения:

pic2.png

Первый вариант предлагался как базовый. Логика проста: отщепляем крайний символ и переводим в тип int. Здесь заведомо есть неоптимальность: нельзя переводить в новый тип «поэлементно». Второй вариант исправляет это. Недостаток обоих вариантов — работают только если знак доллара действительно в конце каждого строкового значения признака. Третий способ более универсален: делаем удаление функцией replace. В четвёртом — обходимся без лямбда-функции. Время выполнения показано на столбцовой гистограмме (здесь и далее, время вычислено для дата-фрейма с числом строк = 10 000 000):

pic3.png

Сразу скажу, что до оптимального по времени варианта студенты не догадались: все положились на функцию replace. Четвёртый вариант, по идее, является улучшением третьего, и тут сюрприз: он в среднем медленнее своего прародителя. Почему-то здесь лямбда-функции работают быстрее интерфейса .str.

Задача — бинаризовать столбец

Совсем простая постановка: в дата-фреме Pandas есть строковый признак из двух значений: A и B. Надо превратить его в бинарный признак, в табл. ниже признак type корректно превращён в type_v1.

pic4.png

Здесь много способов решения:

pic5

Первый способ использует лямбда-функцию (здесь заведомо допущена ошибка с ранним приведение типов — см. предыдущую задачу), второй — обычное сравнение, третий — удобную функцию np.where, четвёртый — универсальную кодировку с помощью словаря (чаще я предпочитаю делать именно так), а следующие три способа — использование специальных функций предобработки данных из разных библиотек. Обратим внимание, что две из них могут выдавать некорректный ответ: получать не нужный признак, а инверсию к нему. Это легко поправить, что мы делать не будем. Кроме того, первые три способа применимы лишь для бинаризации, а другие — легко обобщаются на большее число категорий.

pic6.png

Теперь посмотрим на время бинаризации (см. рис.). Для первого и последнего способа оно не представлено, поскольку существенно уступает другим. Да, LabelEncoder из библиотеки scikit-learn оказался очень медленным (для этой задачи). Самый быстрый способ — через factorize (подумайте, сделает ли его медленнее устранение некорректности?). Ну, а если хотите бинаризовать по-простому: сравнивайте значение с нужным (второй способ).

Задача — расщепить столбец

В строковом столбце дата-фрейма Pandas значения имеют вид «A/B«. Надо расщепить этот столбец на два: в первом будут первые части — A, во втором — B. В табл. ниже столбец A/B корректно расщепляется на A_v1 и B_v1.

pic7.png

Рассмотрим несколько способов решения этой задачи:

pic8.png

Первым способом я раньше пользовался по умолчанию, не представляя, что можно чуть быстрее. Два следующих пытаются реализовать первый «в одну строку»: через приведение к спискам или через приведение к дата-фрейму. Второй способ быстрее первого, а вот третий ожидаемо очень долгий (т.к. получает дата-фрейм и потом его использует для вставки значений в исходный дата-фрейм). Последний способ очень оригинальный (его предложил студент): сильно чувствителен к некорректностям в формате, но, как оказалось, очень быстрый. Надо просто перейти к numpy-матрице.

pic9.png

Задача — заменить пропуски средним

Есть дата-фрейм Pandas, в котором есть служебный признак type — его значения «train» и «test» помечают обучающую и контрольную части выборки. Есть признак feature, в котором довольно много пропусков. Необходимо все пропуски заполнить. Причём, в строках, которые соответствуют обучению, надо заполнить средним значением признака по обучению, аналогично, в контрольных объектах — средним по контролю, см. табл. (в feature_v1 пример корректного заполнения).

pic10.png

Как и раньше, предложим несколько способов:

pic11.png

Первый — простой по своей сути, но очень громоздкий и сложный для понимания. Второй и третий — через волшебную функцию transform. Последний я придумал, когда пытался всё на свете переписать с помощью функции np.where.

pic12.png

По времени победил третий способ, который, правда, кажется мне немного шаманским… я бы при виде этого кода предположил, что будет ошибка. Кстати, раньше его можно было ещё ускорить, но в последней версии Pandas уже нет. Последний, авторский, способ тоже довольно хорош и код, вроде, понятный.

Все листинги, приведённые в этой заметке, можно найти на гитхабе автора. Лучше смотреть так (просмотрщик github показывает некорректно). При подготовке публикации использовались фрагменты кода Глеба Маслякова и Дениса Бибика. В комментариях можно предложить свои способы решения или подкинуть новых задач.

Python и Pandas: делаем быстрее: 15 комментариев

  1. Задача №3 — расщепить столбец, есть еще способ:

    data[‘A_v2’] = data[‘A/B’].apply(lambda x: int(x[:2])).astype(int)
    data[‘B_v2’] = data[‘A/B’].apply(lambda x: int(x[-2:])).astype(int)

    Задача №4 — заменить пропуски средним, есть еще способ:

    dic = data.groupby([‘type’])[‘feature’].mean()
    data = data.merge(dic, on = ‘type’,how = ‘left’, suffixes=(», ‘_mean’))
    data[‘feature_v4’] = data.apply(lambda row: row[‘feature_mean’] if pd.isnull(row[‘feature’]) else row[‘feature’], axis = 1)
    data = data.drop([‘feature_mean’], axis = 1)

    В задаче №4 — заменить пропуски средним, самый быстрый способ у нас почему-то работал только если

    name = ‘feature_v3’ заменить на name = ‘feature’

    Иначе
    df4.loc[df4[name].isnull(), name] = df4.groupby(‘type’)[name].transform(‘mean’)
    выдавал ошибку
    KeyError: ‘Column not found: feature_v3’

    • Задача №3 — не, это плохо. Только для двузначных чисел годится. Изначально задача родилась из обработки признака «давление» (там верхнее и нижнее есть) — там могут быть разные числа.

      Задача №4 — да, но этот какой-то совсем долгий… (3 минуты против 1 секунды)

      В задаче №4 — наверное, из-за разных версий Pandas. Боюсь, у Вас более свежая (почему-то после обновлений все способы, которые раньше были быстрыми перестают работать).

      • Не, в задаче №4 — у Вас просто в тексте статьи нет столбца feature_v3

        Если смотреть код на GITHUB, то там созданы feature_v1, feature_v3 и т.д. и там всё работает

        Я изначально шёл по статье и решал сам, Ваш способ как он тут указан — не работает без переименования столбца.
        Можно просто github ссылку в начало статьи перенести, не править текст.

  2. Задача — бинаризовать столбец еще вариант (хорошо работает на много уникальных значений):
    data[‘type_v4’] = data[‘type’].astype(«category»).cat.codes

  3. В первой задаче еще можно так. В моем случае %timeit показал, что так быстрее всего

    data[«price»].str[:-1].astype(int)

  4. Задача — расщепить столбец

    Не пробовал, но есть apply(lambda x: x.split(‘/’), result_type=’explode’)

  5. По первой задаче есть даже отдельная статья — «5 methods to remove the ‘$’ from your data in Python, and the fastest one» (https://towardsdatascience.com/5-methods-to-remove-the-from-your-data-in-python-and-the-fastest-one-281489382455)
    Если не брать в расчет из статьи самый быстрый и небезопасный способ через преобразование в байты (какой-то он уж совсем хардкорный), можно немного изменить самый быстрый среди остальных и получить такое решение:
    [x[:-1] for x in data[‘price’].values]
    У меня получилось процентов на 30% быстрее остальных способов из ноутбука.

    • В предыдущем комментарии забыл про преобразование в целое число, получается не настолько быстрее как указал, но все равно быстрее всех:
      [int(x[:-1]) for x in data[‘price’].values]

      • Прошу прощения за спам, оказалось что у меня оно быстрее всего, но до присвоения к колонке dataframe, с присвоением становится хуже части решений, поэтому не самое быстрое получилось 🙂

      • Правильное решение (и самое быстрое среди всех у меня):
        data[‘price($)_v6’] = np.array([x[:-1] for x in data[‘price’].values], dtype=’int’)

  6. Задача 2:
    (добавил в одно из решений перевод в np.array, дало наилучшую скорость в итоге):
    data[‘type_v8’] = np.where(data[‘type’].values == ‘A’, 1, 0)

    Задача 4:
    Быстрее всего получился следующий вариант:
    data[name].fillna(data.groupby(‘type’)[name].transform(‘mean’), inplace=True)

    Вариант
    data[name] = data[name].fillna(data.groupby(‘type’)[name].transform(‘mean’))
    тоже быстрее всех из ноутбука (на моем компьютере), но inplace чуть побыстрее

Добавить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход /  Изменить )

Google photo

Для комментария используется ваша учётная запись Google. Выход /  Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход /  Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход /  Изменить )

Connecting to %s