DevGang
Авторизоваться

Как увеличить скорость Pandas и обрабатывать 10 млн необработанных наборов данных за миллисекунды

За последние годы использование Pandas выросло в бесчисленное количество раз. Покажу вам, как использовать Pandas максимально быстрым способом. Потому что вы не можете ускорить то, что и так быстро работает.

Было бы странным использовать функции, которые в документации Pandas четко указаны как медленные, а не как самые быстрые методы.

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

Эффективное индексирование

Давайте начнем с самых простых операций. В частности, мы увидим самый быстрый способ выбора строк и столбцов, Вам известно, что в Pandas есть два оператора индексации — loc и iloc. Несмотря на то, что их разница не будет иметь большого значения для небольших наборов данных, они будут довольно заметны по мере увеличения размера данных.

Во-первых, для выбора строки или нескольких строк iloc работает быстрее.

tps = pd.read_csv("data/train.csv")

>>> tps.shape
(957919, 120)

# Choose rows
>>> tps.iloc[range(10000)]

Напротив, loc лучше всего подходит для выбора столбцов с их метками

>>> tps.loc[:, ["f1", "f2", "f3"]]

Для выборки столбцов иои строк встроенная функция выборки является самой быстрой. Вы можете подумать, что выборка с использованием случайных индексов с помощью NumPy будет быстрее, но это не так.

# Sampling rows
>>> tps.sample(7, axis=0)
# Sampling 5 columns and 7 rows
>>> tps.sample(5, axis=1).sample(7, axis=0)

Эффективная замена значений

Большую часть времени я вижу, как люди используя loc или iloc для замены определенных значений в DataFrame:

adult_income = pd.read_csv("data/adult.csv")

adult_income.loc[adult_income["workclass"] == "?", "workclass"] = np.nan

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

adult_income.replace(to_replace="?", value=np.nan, inplace=True)

Хотя скорость является первым преимуществом замены (replace), вторым является его гибкость. Выше мы заменили все вопросительные знаки на NaN-операцию, которая потребовала бы нескольких вызовов с заменой на основе индекса. 

Кроме того, replace позволяет использовать списки или словари для одновременного изменения нескольких значений.

adult_income.replace(["Male", "Female"], ["M", "F"], inplace=True)

При замене списка значений другим они будут иметь взаимно однозначное сопоставление индекса с индексом. Вы можете быть более конкретными со словарями, такими как:

adult_income.replace({"United States": "USA", "US": "USA"}, inplace=True)

Можно сделать еще более детализированным использование вложенных словарей.

adult_income.replace(
    {
        "education": {"HS-grad": "High school", "Some-college": "College"},
        "income": {"<=50K": 0, ">50K": 1},
    },
    inplace=True,
)

Вложенная замена помогает, когда вы хотите повлиять только на значения определенных столбцов. Здесь мы заменяем значения только в столбцах education и income.

Существуют и другие преимущества replace, включая замену на основе регулярных выражений, о которых можно прочитать в документации

Эффективное повторение

Золотое правило применения операция ко всем столбцам или фреймам данных - никогда не использовать циклы. Это шаблонный совет, который работает. Но я понимаю - как вы манипулируете целыми массивами без зацикливания?

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

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

Итак, если вы хотите выполнить какую-либо математическую операцию над одним или несколькими столбцами, есть большая вероятность, что операция векторизована в  Pandas. Напимер, встроеные операторы Python, такие как +, -, *, /, ** работайте также как и с векторами.

Чтобы получить представление о векторизации, давайте выполним некоторые операции с массивным набором данных. Мы веберем набор данных  ~1 МЛН строк для сентябрьского конкурса Kaggle TPS:

import datatable as dt

tps = dt.fread("data/train.csv").to_pandas()
>>> tps.shape
(957919, 120)

Давайте выполним некоторые математические операции над несколькими столбцами с помощью функции Pandas apply. Это самый быстрый встроенный итератор Pandas.

def crazy_function(col1, col2, col3):
    return np.sqrt(col1 ** 3 + col2 ** 2 + col3 * 10)

Мы запустим эту функцию в трех столбцах с помощью apply и проверим время выполнения.


%time tps['f1000'] = tps.apply(lambda row: crazy_function(
                                row['f1'], row['f56'], row['f44']
                              ), axis=1)

Wall time: 19.3 s

19,3 секунды для набора данных в 1 млн строк. Теперь посмотрите, что происходит, когда мы передаем столбцы в виде векторов, а не скаляров.Нет необходимости изменять функцию: 


%time tps['f1001'] = crazy_function(tps['f1'], tps['f56'], tps['f44'])

Wall time: 37 ms

Засекаем время - 37 миллисекунд! Это примерно в 600 раз быстрее, чем самый быстрый итератор. Но мы можем сделать еще лучше - векторизация выполняется еще быстрее при использовании с массивами NumPy:

%time tps['f1001'] = crazy_function(
          tps['f1'].values, tps['f56'].values, tps['f44'].values
        )

Wall time: 34 ms

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

Чем больше, тем быстрее

У Pandas есть еще несколько трюков в рукаве. Справедливое предупреждение - это не принесет вам большей пользы, если у вас не будет более + 1 млн строк. 

Во-первых, мы увеличиваем размер набора данных в десять раз. чтобы бросить себе вызов 

massive_df = pd.concat([tps.drop(["f1000", "f1001"], axis=1)] * 10)

>>> massive_df.shape
(9579190, 120)

memory_usage = massive_df.memory_usage(deep=True)
memory_usage_in_mbs = np.sum(memory_usage / 1024 ** 2)

>>> memory_usage_in_mbs
8742.604093551636

Около 9 ГБ — этого должно быть достаточно. 

Теперь мы продолжим использовать crazy_function, начиная с векторизации NumPy в качестве базовой линии 


%%time

massive_df["f1001"] = crazy_function(
    massive_df["f1"].values, massive_df["f56"].values, massive_df["f44"].values
)

Wall time: 324 ms

Около 0,3 секунды для набора данных из 10 млн строк. Когда попробовал с помощью apply, это не закончилось лаже через час

Теперь давайте еще больше улучшим время выполнения. Первый кандидат - Numba. МП устанавливаем его через pip (pip install numba) и импортируем его. Затем мы украсим нашу crazy_function ее jit-функцией. JIT расшифровывается как just in time, и он переводит чистый код Python и NumPy в собственные машинные инструкции, что значительно ускоряет работу. 

import numba

@numba.jit
def crazy_function(col1, col2, col3):
    return (col1 ** 3 + col2 ** 2 + col3 * 10) ** 0.5

Теперь мы запускаем его так же, как и любую другую функцию:


%%time

massive_df["f1001"] = crazy_function(
    massive_df["f1"].values, massive_df["f56"].values, massive_df["f44"].values
)

Wall time: 201 ms

Мы добились ускорения примерно в 1,5 раза. Numba лучше всего работает с функциями, которые включают в себя множество циклов Python, много математики, и что еще лучше, функции NumPy и массивы.

Numba может делать гораздо больше, включая параллельные вычисления и компеляцию на основе GPU.

Если вас не интересуют дополнительные зависимости, позвольте познакомить вас с eval в Pandas. Существует две версии - pd.eval (более высокого уровня) и df.eval (в контексте фреймов данных). Как и в Numba, у вас доллжно быть не меннее + 10 000 выборок в фрейме данных, чтобы увидеть улучшения. Но как только вы это сделаете, вы увидите значительные преимущества в скорости.

Давайте запустим нашу crazy_function в контексте df.eval:

%%time

massive_df.eval("f1001 = (f1 ** 3 + f56 ** 2 + f44 * 10) ** 0.5", inplace=True)

Wall time: 651 ms

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

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

Обюратите внимание, что мы говорили в диапазоне милисекунд даже для набора данных в 10 млн  строк.

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

#Python #Pandas
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В этом месте могла бы быть ваша реклама

Разместить рекламу