Как увеличить скорость 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 млн строк.
Мы рассмотрели самые важные и эффективные методы, не обращая внимания на мелкие детали. Рекомендую ознакомиться с ним для получения дополнительных советов по увеличению скорости.