Быстрый, гибкий, простой и интуитивно понятный: Как ускорить ваши проекты pandas

Оглавление

Если вы работаете с большими массивами данных, вы, вероятно, помните момент “ага” в своем путешествии по Python, когда вы обнаружили библиотеку pandas. pandas кардинально меняет правила игры для науки о данных и аналитики, особенно если вы пришли к Python, потому что искали что-то более мощное, чем Excel и VBA.

Так что же такого есть в pandas, что вызывает восторг у специалистов по обработке данных, аналитиков и инженеров вроде меня? Ну, в документации pandas говорится, что он использует:

быстрые, гибкие и выразительные структуры данных, разработанные для того, чтобы сделать работу с “реляционными” или “помеченными” данными одновременно простой и интуитивно понятной.”

Быстрый, гибкий, простой и интуитивно понятный? Звучит заманчиво! Если ваша работа связана с созданием сложных моделей данных, вы не захотите тратить половину времени на разработку, ожидая, пока модули обработают большие массивы данных. Вы хотите посвятить свое время и интеллектуальные способности интерпретации ваших данных, а не кропотливой работе с менее мощными инструментами.

Но Я Слышал, что панды Медлительны...

Когда я впервые начал использовать pandas, мне сообщили, что, хотя это отличный инструмент для анализа данных, pandas слишком медленный для использования в качестве инструмента статистического моделирования. На начальном этапе это оказалось правдой. Я потратил больше нескольких минут, бездельничая, ожидая, пока pandas обработает данные.

Но потом я узнал, что pandas построен поверх структуры массива NumPy, и поэтому многие его операции выполняются на C, либо с помощью NumPy, либо с помощью собственной библиотеки pandas , из Python модули расширения, написанные на Cython и скомпилированные в C. Так разве панды тоже не должны быть быстрыми?

Так и должно быть, если вы используете его так, как оно было задумано!

Парадокс заключается в том, что то, что в противном случае могло бы “выглядеть” как код на Python, может быть неоптимальным в pandas с точки зрения эффективности. Как и NumPy, pandas предназначен для векторизованных операций, которые обрабатывают целые столбцы или наборы данных за один проход. Рассмотрение каждой “ячейки” или строки в отдельности, как правило, должно быть последним средством, а не первым.

Это руководство

Чтобы было понятно, это не руководство по чрезмерной оптимизации кода pandas. pandas уже создан для быстрой работы при правильном использовании. Кроме того, существует большая разница между оптимизацией и написанием чистого кода.

Это руководство по использованию pandas на языке Python для получения максимальной отдачи от его мощных и простых в использовании встроенных функций. Кроме того, вы узнаете несколько практических советов по экономии времени, так что вам не придется тратить время впустую каждый раз, когда вы будете работать со своими данными.

В этом руководстве вы узнаете следующее:

  • Преимущества использования datetime данных с временными рядами
  • Наиболее эффективный способ выполнения пакетных вычислений
  • Экономия времени за счет хранения данных с помощью HDFStore

Чтобы продемонстрировать эти темы, я приведу пример из своей повседневной работы, в котором рассматривается временной ряд потребления электроэнергии. После загрузки данных вы последовательно перейдете к более эффективным способам получения конечного результата. Для большинства pandas справедливо утверждение, что существует несколько способов перейти от пункта А к пункту Б. Однако это не означает, что все доступные варианты одинаково хорошо масштабируются для более крупных и требовательных наборов данных.

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

Текущая задача

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

Давайте прочитаем наши данные из CSV-файла, который содержит два столбца: один для даты и времени, а другой для потребляемой электроэнергии в киловатт-часах (кВтч):

CSV data

В строках указано количество электроэнергии, использованной за каждый час, таким образом, имеется 365 x 24 = 8760 строк за весь год. Каждая строка указывает на использование “начального часа” в данный момент, таким образом, 1/1/13 0:00 указывает на использование в течение первого часа 1 января.

Экономия времени с помощью данных Datetime

Первое, что вам нужно сделать, это прочитать ваши данные из CSV-файла с помощью одной из функций ввода-вывода pandas:

>>> import pandas as pd
>>> pd.__version__
'0.23.1'

# Make sure that `demand_profile.csv` is in your
# current working directory.
>>> df = pd.read_csv('demand_profile.csv')
>>> df.head()
     date_time  energy_kwh
0  1/1/13 0:00       0.586
1  1/1/13 1:00       0.580
2  1/1/13 2:00       0.572
3  1/1/13 3:00       0.596
4  1/1/13 4:00       0.592


На первый взгляд все выглядит нормально, но есть небольшая проблема. в pandas и NumPy есть понятие dtypes (типы данных). Если аргументы не указаны, date_time примет значение object dtype:

>>> df.dtypes
date_time      object
energy_kwh    float64
dtype: object

>>> type(df.iat[0, 0])
str


Это не идеально. object является контейнером не только для str, но и для любого столбца, который не может быть четко вписан в один тип данных. Было бы сложно и неэффективно работать с датами в виде строк. (Это также было бы неэффективно с точки зрения памяти.)

Для работы с данными временных рядов вам потребуется, чтобы столбец date_time был отформатирован как массив объектов datetime. (pandas называет это Timestamp.) pandas делает каждый шаг здесь довольно простым:

>>> df['date_time'] = pd.to_datetime(df['date_time'])
>>> df['date_time'].dtype
datetime64[ns]


(Обратите внимание, что в качестве альтернативы вы могли бы использовать панд PeriodIndex в этом случае.)

Теперь у вас есть фрейм данных под названием df, который очень похож на наш CSV-файл. В нем есть два столбца и числовой индекс для ссылок на строки.

>>> df.head()
               date_time    energy_kwh
0    2013-01-01 00:00:00         0.586
1    2013-01-01 01:00:00         0.580
2    2013-01-01 02:00:00         0.572
3    2013-01-01 03:00:00         0.596
4    2013-01-01 04:00:00         0.592


Приведенный выше код прост и понятен, но насколько он быстр? Давайте протестируем это с помощью timing decorator, который я неоригинально назвал @timeit. Этот декоратор в значительной степени имитирует timeit.repeat() из стандартной библиотеки Python, но позволяет возвращать результат работы самой функции и выводить среднее время ее выполнения из нескольких попыток. (timeit.repeat() в Python возвращает результаты синхронизации, а не результат функции.)

Создание функции и размещение @timeit декоратора непосредственно над ней будет означать, что каждый раз, когда функция вызывается, она будет синхронизироваться по времени. Декоратор запускает внешний и внутренний циклы:

>>> @timeit(repeat=3, number=10)
... def convert(df, column_name):
...     return pd.to_datetime(df[column_name])

>>> # Read in again so that we have `object` dtype to start 
>>> df['date_time'] = convert(df, 'date_time')
Best of 3 trials with 10 function calls per trial:
Function `convert` ran in average of 1.610 seconds.


Результат? 8760 строк данных обрабатываются за 1,6 секунды. “Отлично”, - скажете вы, - “это совсем немного времени”. Но что, если вы столкнетесь с большими массивами данных — скажем, об использовании электроэнергии за один год с интервалом в одну минуту? Это в 60 раз больше данных, поэтому вам придется ждать около полутора минут. Это начинает казаться менее приемлемым.

На самом деле, недавно я проанализировал почасовые данные о потреблении электроэнергии за 10 лет с 330 сайтов. Вы думаете, я ждал 88 минут, чтобы перевести даты и время? Ни в коем случае!

Как вы можете ускорить это? Как правило, pandas работает тем быстрее, чем меньше ему приходится интерпретировать ваши данные. В этом случае вы увидите значительное повышение скорости, просто сообщив pandas, как выглядят ваши данные о времени и дате, используя параметр format. Вы можете сделать это, используя strftime коды, найденные здесь и введя их следующим образом:

>>> @timeit(repeat=3, number=100)
>>> def convert_with_format(df, column_name):
...     return pd.to_datetime(df[column_name],
...                           format='%d/%m/%y %H:%M')
Best of 3 trials with 100 function calls per trial:
Function `convert_with_format` ran in average of 0.032 seconds.


Новый результат? 0,032 секунды, что в 50 раз быстрее! Таким образом, вы только что сэкономили около 86 минут времени обработки для моих 330 сайтов. Неплохое улучшение!

Еще одна важная деталь заключается в том, что даты и время в CSV не указаны в формате ISO 8601: вам понадобится YYYY-MM-DD HH:MM. Если вы не укажете формат, pandas будет использовать пакет dateutil для преобразования каждой строки в дату.

И наоборот, если исходные данные datetime уже представлены в формате ISO 8601, pandas может немедленно перейти к быстрому анализу дат. Это одна из причин, по которой здесь так полезно четко указывать формат. Другой вариант - передать параметр infer_datetime_format=True, но, как правило, стоит указывать явно.

Внимание: панды’ read_csv() также позволяет анализировать даты в составе файлового ввода-вывода шаг. Смотрите параметры parse_dates, infer_datetime_format, и date_parser.

Простое зацикливание на данных pandas

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

Tariff Type Cents per kWh Time Range
Peak 28 17:00 to 24:00
Shoulder 20 7:00 to 17:00
Off-Peak 12 0:00 to 7:00

Если бы цена была фиксированной - 28 центов за киловатт-час в течение каждого часа дня, большинство людей, знакомых с pandas, знали бы, что этот расчет можно выполнить в одной строке:

>>> df['cost_cents'] = df['energy_kwh'] * 28


В результате будет создан новый столбец со стоимостью электроэнергии за этот час:

               date_time    energy_kwh       cost_cents
0    2013-01-01 00:00:00         0.586           16.408
1    2013-01-01 01:00:00         0.580           16.240
2    2013-01-01 02:00:00         0.572           16.016
3    2013-01-01 03:00:00         0.596           16.688
4    2013-01-01 04:00:00         0.592           16.576
# ...


Но наш расчет стоимости зависит от времени суток. Именно здесь вы увидите, что многие люди используют pandas не так, как предполагалось: путем написания цикла для выполнения условного расчета.

В оставшейся части этого руководства вы начнете с неидеального базового решения и перейдете к решению на Python, которое полностью использует pandas.

Но что такое Pythonic в случае с пандами? Ирония заключается в том, что именно те, кто имеет опыт работы с другими (менее удобными для пользователя) языками программирования, такими как C++ или Java, особенно подвержены этому, потому что они инстинктивно “мыслят циклами”.

Давайте рассмотрим циклический подход, который не основан на Python и который многие люди используют, когда не знают, для чего предназначен pandas. Мы снова воспользуемся @timeit, чтобы увидеть, насколько быстр этот подход.

Сначала давайте создадим функцию для применения соответствующего тарифа к данному часу:

def apply_tariff(kwh, hour):
    """Calculates cost of electricity for given hour."""    
    if 0 <= hour < 7:
        rate = 12
    elif 7 <= hour < 17:
        rate = 20
    elif 17 <= hour < 24:
        rate = 28
    else:
        raise ValueError(f'Invalid hour: {hour}')
    return rate * kwh


Вот цикл, который не является питоновским, во всей его красе:

>>> # NOTE: Don't do this!
>>> @timeit(repeat=3, number=100)
... def apply_tariff_loop(df):
...     """Calculate costs in loop.  Modifies `df` inplace."""
...     energy_cost_list = []
...     for i in range(len(df)):
...         # Get electricity used and hour of day
...         energy_used = df.iloc[i]['energy_kwh']
...         hour = df.iloc[i]['date_time'].hour
...         energy_cost = apply_tariff(energy_used, hour)
...         energy_cost_list.append(energy_cost)
...     df['cost_cents'] = energy_cost_list
... 
>>> apply_tariff_loop(df)
Best of 3 trials with 100 function calls per trial:
Function `apply_tariff_loop` ran in average of 3.152 seconds.


Для людей, которые выбрали pandas после того, как некоторое время назад написали “чистый Python”, этот дизайн может показаться естественным: у вас есть типичный “для каждого x, при условии y, do z.”

Однако этот цикл является громоздким. Вы можете считать вышеописанное “антипаттерном” в pandas по нескольким причинам. Во-первых, ему необходимо инициализировать список, в который будут записываться выходные данные.

Во-вторых, он использует непрозрачный объект range(0, len(df)) для выполнения цикла, а затем, после применения apply_tariff(), он должен добавить результат в список, который используется для создания нового столбца фрейма данных. Он также выполняет то, что называется цепной индексацией с df.iloc[i]['date_time'], что часто приводит к непредвиденным результатам.

Но самая большая проблема с этим подходом - временные затраты на вычисления. На моем компьютере этот цикл занял более 3 секунд для обработки 8760 строк данных. Далее вы рассмотрите некоторые улучшенные решения для итерации по структурам pandas.

Зацикливание с помощью .itertuples() и .iterrows()

Какие еще подходы вы можете использовать? Что ж, pandas фактически сделал синтаксис for i in range(len(df)) избыточным, введя методы DataFrame.itertuples() и DataFrame.iterrows(). Это оба метода генератора, которые yield обрабатывают по одной строке за раз.

.itertuples() возвращает значение namedtuple для каждой строки со значением индекса строки в качестве первого элемента кортежа. nametuple - это структура данных из модуля collections Python, которая ведет себя как кортеж Python, но имеет поля, доступные с помощью поиска по атрибутам.

.iterrows() возвращает пары (кортежи) значений (index, Series) для каждой строки во фрейме данных.

Хотя .itertuples() обычно получается немного быстрее, давайте остановимся на pandas и используем .iterrows() в этом примере, потому что некоторые читатели, возможно, не сталкивались с nametuple. Давайте посмотрим, чего это дает:

>>> @timeit(repeat=3, number=100)
... def apply_tariff_iterrows(df):
...     energy_cost_list = []
...     for index, row in df.iterrows():
...         # Get electricity used and hour of day
...         energy_used = row['energy_kwh']
...         hour = row['date_time'].hour
...         # Append cost list
...         energy_cost = apply_tariff(energy_used, hour)
...         energy_cost_list.append(energy_cost)
...     df['cost_cents'] = energy_cost_list
...
>>> apply_tariff_iterrows(df)
Best of 3 trials with 100 function calls per trial:
Function `apply_tariff_iterrows` ran in average of 0.713 seconds.


Были достигнуты некоторые незначительные улучшения. Синтаксис стал более понятным, и в ссылках на значения строк стало меньше беспорядка, что делает его более читабельным. С точки зрения экономии времени, это почти в 5 раз быстрее!

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

панды’ .apply()

Вы можете еще больше улучшить эту операцию, используя метод .apply() вместо метода .iterrows(). Метод .apply() от pandas принимает функции (вызываемые объекты) и применяет их вдоль оси фрейма данных (ко всем строкам, или все столбцы). В этом примере лямбда-функция поможет вам передать два столбца данных в apply_tariff():

>>> @timeit(repeat=3, number=100)
... def apply_tariff_withapply(df):
...     df['cost_cents'] = df.apply(
...         lambda row: apply_tariff(
...             kwh=row['energy_kwh'],
...             hour=row['date_time'].hour),
...         axis=1)
...
>>> apply_tariff_withapply(df)
Best of 3 trials with 100 function calls per trial:
Function `apply_tariff_withapply` ran in average of 0.272 seconds.


Синтаксические преимущества .apply() очевидны, они заключаются в значительном сокращении количества строк и очень удобочитаемом, понятном коде. В этом случае затраченное время было примерно вдвое меньше, чем при использовании метода .iterrows().

Однако это еще не “невероятно быстро”. Одна из причин заключается в том, что .apply() будет пытаться выполнить внутренний цикл по итераторам Cython. Но в этом случае lambda, который вы передали, не может быть обработан в Cython, поэтому он вызывается в Python, что, следовательно, не так быстро.

Если бы вы использовали .apply() для моих 10-летних ежечасных данных по 330 сайтам, вам потребовалось бы около 15 минут на обработку. Если бы этот расчет был задуман как небольшая часть более крупной модели, вы бы действительно захотели ускорить процесс. Вот где пригодятся векторизованные операции.

Выбор данных С помощью .isin()

Ранее вы видели, что если бы существовала единая цена на электроэнергию, вы могли бы применить эту цену ко всем данным о потреблении электроэнергии в одной строке кода (df['energy_kwh'] * 28). Эта конкретная операция была примером векторизованной операции, и это самый быстрый способ выполнения задач в pandas.

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

В следующем примере вы увидите, как выбрать строки с помощью метода pandas .isin(), а затем применить соответствующий тариф в векторизованной операции. Прежде чем вы это сделаете, вам будет немного удобнее, если вы зададите столбец date_time в качестве индекса фрейма данных:

df.set_index('date_time', inplace=True)

@timeit(repeat=3, number=100)
def apply_tariff_isin(df):
    # Define hour range Boolean arrays
    peak_hours = df.index.hour.isin(range(17, 24))
    shoulder_hours = df.index.hour.isin(range(7, 17))
    off_peak_hours = df.index.hour.isin(range(0, 7))

    # Apply tariffs to hour ranges
    df.loc[peak_hours, 'cost_cents'] = df.loc[peak_hours, 'energy_kwh'] * 28
    df.loc[shoulder_hours,'cost_cents'] = df.loc[shoulder_hours, 'energy_kwh'] * 20
    df.loc[off_peak_hours,'cost_cents'] = df.loc[off_peak_hours, 'energy_kwh'] * 12


Давайте посмотрим, как это соотносится:

>>> apply_tariff_isin(df)
Best of 3 trials with 100 function calls per trial:
Function `apply_tariff_isin` ran in average of 0.010 seconds.


Чтобы понять, что происходит в этом коде, вам нужно знать, что метод .isin() возвращает массив логических значений, который выглядит следующим образом:

[False, False, False, ..., True, True, True]


Эти значения определяют, какие индексы фреймов данных (даты и время) попадают в указанный часовой диапазон. Затем, когда вы передаете эти логические массивы индексатору .loc фрейма данных, вы получаете фрагмент фрейма данных, который содержит только строки, соответствующие этим часам. После этого остается просто умножить срез на соответствующий тариф, что представляет собой быструю векторизованную операцию.

Как это соотносится с нашими циклическими операциями, описанными выше? Во-первых, вы можете заметить, что вам больше не нужен apply_tariff(), потому что при выборе строк применяется вся условная логика. Таким образом, количество строк кода, которые вы должны написать, и количество вызываемого кода на Python значительно сокращаются.

А как насчет времени обработки? В 315 раз быстрее, чем в цикле, который не был выполнен на языке Python, примерно в 71 раз быстрее, чем .iterrows() и в 27 раз быстрее, чем .apply(). Теперь вы двигаетесь с той скоростью, которая вам необходима, чтобы легко и быстро справляться с большими массивами данных.

Можем Ли Мы Добиться Большего Успеха?

В apply_tariff_isin() мы, по общему признанию, все еще выполняем некоторую “ручную работу”, вызывая df.loc и df.index.hour.isin() по три раза каждый. Вы могли бы возразить, что это решение не масштабируемо, если бы у нас был более детальный диапазон временных интервалов. (При разной скорости для каждого часа потребовалось бы 24 .isin() звонков.) К счастью, в этом случае вы можете делать все еще более программно с помощью функции pandas pd.cut():

@timeit(repeat=3, number=100)
def apply_tariff_cut(df):
    cents_per_kwh = pd.cut(x=df.index.hour,
                           bins=[0, 7, 17, 24],
                           include_lowest=True,
                           labels=[12, 20, 28]).astype(int)
    df['cost_cents'] = cents_per_kwh * df['energy_kwh']


Давайте на секунду разберемся, что здесь происходит. pd.cut() применяется набор меток (наши затраты) в соответствии с тем, к какой ячейке относится каждый час. Обратите внимание, что параметр include_lowest указывает, должен ли первый интервал включаться слева или нет. (Вы хотите включить time=0 в группу.)

Это полностью векторизованный способ достижения желаемого результата, и он выходит на первое место по срокам:

>>> apply_tariff_cut(df)
Best of 3 trials with 100 function calls per trial:
Function `apply_tariff_cut` ran in average of 0.003 seconds.


На данный момент обработка всего набора данных из 300 сайтов занимает от более часа до менее секунды. Неплохо! Однако есть еще один, последний вариант, который заключается в использовании функций NumPy для управления базовыми массивами NumPy для каждого фрейма данных, а затем для интеграции результатов обратно в структуры данных pandas.

Не забудь про NumPy!

При использовании pandas не следует забывать о том, что серии pandas и фреймы данных разработаны на основе библиотеки NumPy. Это обеспечивает еще большую гибкость вычислений, поскольку pandas легко работает с массивами и операциями NumPy.

В следующем случае вы будете использовать функцию NumPy digitize(). Это похоже на cut() в pandas в том смысле, что данные будут сгруппированы, но на этот раз они будут представлены массивом индексов, представляющих, к какой ячейке относится каждый час. Затем эти индексы применяются к массиву цен:

@timeit(repeat=3, number=100)
def apply_tariff_digitize(df):
    prices = np.array([12, 20, 28])
    bins = np.digitize(df.index.hour.values, bins=[7, 17, 24])
    df['cost_cents'] = prices[bins] * df['energy_kwh'].values


Как и функция cut(), этот синтаксис удивительно лаконичен и прост для чтения. Но каково его быстродействие? Давайте посмотрим:

>>> apply_tariff_digitize(df)
Best of 3 trials with 100 function calls per trial:
Function `apply_tariff_digitize` ran in average of 0.002 seconds.


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

С помощью pandas можно, если хотите, поддерживать “иерархию” предпочтительных вариантов пакетных вычислений, как вы сделали здесь. Обычно они ранжируются от самых быстрых к самым медленным (и от наиболее гибких к наименее гибким):

  1. Используйте векторизованные операции: методы и функции pandas без for циклов.
  2. Используйте метод .apply() с вызываемым элементом.
  3. Используйте .itertuples(): выполните итерацию по строкам фрейма данных как namedtuples из модуля collections Python.
  4. Используйте .iterrows(): выполняйте итерацию по строкам фрейма данных в виде пар (индекс, pd.Series). Хотя серия pandas представляет собой гибкую структуру данных, объединение каждой строки в серию и последующий доступ к ней могут быть дорогостоящими.
  5. Используйте “поэлементно” для циклов, обновляя каждую ячейку или строку по очереди с помощью df.loc или df.iloc. (Или, .at/.iat для быстрого скалярного доступа.)

Не верьте Мне На Слово: Порядок следования, приведенный выше, является предложением непосредственно от разработчика core pandas.

Вы можете узнать больше о своих возможностях в разделе Как выполнять итерацию по строкам в pandas и почему этого не следует делать.

Вот приведенный выше “порядок приоритета” для каждой функции, которую вы создали здесь:

Function Runtime (seconds)
apply_tariff_loop() 3.152
apply_tariff_iterrows() 0.713
apply_tariff_withapply() 0.272
apply_tariff_isin() 0.010
apply_tariff_cut() 0.003
apply_tariff_digitize() 0.002

Предотвратите повторную обработку с помощью HDFStore

Теперь, когда вы ознакомились с быстрой обработкой данных в pandas, давайте рассмотрим, как вообще избежать времени на повторную обработку с помощью HDFStore, который недавно был интегрирован в pandas.

Часто, когда вы создаете сложную модель данных, бывает удобно выполнить некоторую предварительную обработку ваших данных. Например, если у вас есть данные о минутном потреблении электроэнергии за 10 лет, простое преобразование даты и времени в datetime может занять 20 минут, даже если вы укажете параметр формата. На самом деле вы хотите сделать это только один раз, а не каждый раз, когда запускаете свою модель для тестирования или анализа.

Очень полезная вещь, которую вы можете здесь сделать, - это предварительно обработать, а затем сохранить ваши данные в обработанном виде, чтобы использовать их при необходимости. Но как вы можете хранить данные в нужном формате без необходимости их повторной обработки? Если бы вы сохранили файл в формате CSV, вы бы просто потеряли свои объекты datetime, и вам пришлось бы повторно обрабатывать их при повторном доступе.

в pandas есть встроенное решение для этого, которое использует HDF5, высокопроизводительный формат хранения, разработанный специально для хранения табличных массивов данных. Pandas’ HDFStore класс позволяет сохранять ваш фрейм данных в формате HDF5, чтобы к нему можно было эффективно обращаться, сохраняя при этом типы столбцов и другие метаданные. Это класс, похожий на словарь, поэтому вы можете читать и писать точно так же, как для объекта Python dict.

Вот как вы могли бы сохранить предварительно обработанный фрагмент данных о потреблении электроэнергии, df, в файле HDF5:

# Create storage object with filename `processed_data`
data_store = pd.HDFStore('processed_data.h5')

# Put DataFrame into the object setting the key as 'preprocessed_df'
data_store['preprocessed_df'] = df
data_store.close()


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

# Access data store
data_store = pd.HDFStore('processed_data.h5')

# Retrieve data using key
preprocessed_df = data_store['preprocessed_df']
data_store.close()


Хранилище данных может содержать несколько таблиц, имя каждой из которых является ключом.

Просто замечание об использовании HDFStore в pandas: у вас должны быть установлены PyTables >= 3.0.0, поэтому после установки pandas обязательно обновите PyTables следующим образом:

pip install --upgrade tables


Выводы

Если вы не уверены, что ваш проект pandas будет быстрым, гибким, простым и интуитивно понятный подумайте о том, чтобы переосмыслить то, как вы используете библиотеку.

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

  • Старайтесь использовать векторизованные операции там, где это возможно, вместо того, чтобы подходить к проблемам с точки зрения for x in df.... Если в вашем коде содержится много for циклов, возможно, он лучше подходит для работы с собственными структурами данных Python, поскольку в противном случае pandas сопряжен с большими накладными расходами.

  • Если у вас есть более сложные операции, где векторизация просто невозможна или слишком сложна для эффективного выполнения, используйте метод .apply().

  • Если вам все-таки приходится перебирать массив (что случается), используйте .iterrows() или .itertuples() для повышения скорости и улучшения синтаксиса.

  • в pandas много вариантов, и почти всегда есть несколько способов добраться из пункта А в пункт Б. Помните об этом, сравните эффективность различных маршрутов и выберите тот, который лучше всего подходит для вашего проекта.

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

  • Интеграция NumPy в операции pandas часто может повысить скорость и упростить синтаксис.

<статус завершения article-slug="fast-flexible-pandas" class="btn-group mb-0" data-api-article-bookmark-url="/api/v1/articles/fast-flexible-pandas/bookmark/" данные-api-article-завершение-статус-url="/api/версия 1/статьи/быстрые и гибкие-pandas/завершение_статуса/"> <кнопка поделиться bluesky-text="Интересная статья на #Python от @realpython.com :" email-body="Ознакомьтесь с этой статьей на Python:%0A%0AFast, гибкий, простой и интуитивно понятный: как ускорить ваши проекты на pandas" email-subject="Статья на Python для вас" twitter-text="Интересная статья на #Python от @realpython:" url="https://realpython.com/fast-flexible-pandas/" url-title="Быстрый, гибкий, простой и интуитивно понятный: как ускорить ваши проекты на pandas"> Back to Top