Pandas не нуждается в представлении: на сегодняшний день это главный инструмент для анализа данных на Python. Я работаю специалистом по анализу данных, и несмотря на то, что пользуюсь pandas каждый день, не перестаю удивляться разнообразию функционала этой библиотеки. В этой статье я хочу рассказать о пяти малоизвестных функциях pandas, которые я недавно узнал и теперь продуктивно использую.
Для новичков: Pandas — это высокопроизводительный набор инструментов для анализа данных на Python с простыми и удобными структурами данных. Название произошло от понятия «panel data», эконометрического термина, которым называют данные о наблюдениях одних и тех же субъектов в течение разных периодов времени.
Здесь можно скачать Jupyter Notebook с примерами из статьи.
1. Диапазоны дат [Date Ranges]
Нередко нужно указывать диапазоны дат при запросе данных из внешнего API или базы данных. Pandas нас не бросит в беде. Как раз для этих случаев есть функция data_range, которая возвращает массив дат, увеличенных на дни, месяцы, года, и т.д.
Скажем, нам нужен диапазон дат по дням:
date_from = "2019-01-01"
date_to = "2019-01-12"
date_range = pd.date_range(date_from, date_to, freq="D")
date_range
Преобразуем сгенерированный
date_range
в пары дат «от» и «до», которые можно будет передать в соответствующую функцию.for i, (date_from, date_to) in enumerate(zip(date_range[:-1], date_range[1:]), 1):
date_from = date_from.date().isoformat()
date_to = date_to.date().isoformat()
print("%d. date_from: %s, date_to: %s" % (i, date_from, date_to))
1. date_from: 2019-01-01, date_to: 2019-01-02
2. date_from: 2019-01-02, date_to: 2019-01-03
3. date_from: 2019-01-03, date_to: 2019-01-04
4. date_from: 2019-01-04, date_to: 2019-01-05
5. date_from: 2019-01-05, date_to: 2019-01-06
6. date_from: 2019-01-06, date_to: 2019-01-07
7. date_from: 2019-01-07, date_to: 2019-01-08
8. date_from: 2019-01-08, date_to: 2019-01-09
9. date_from: 2019-01-09, date_to: 2019-01-10
10. date_from: 2019-01-10, date_to: 2019-01-11
11. date_from: 2019-01-11, date_to: 2019-01-12
2. Слияние с индикатором источника [Merge with indicator]
Слияние двух наборов данных — это, как ни странно, процесс объединения двух наборов данных в один, строки которых сопоставляются на основании общих колонок или свойств.
Один из двух аргументов функции слияния, который я как-то упустил, —
indicator
. «Индикатор» добавляет колонку _merge
в DataFrame, которая показывает, откуда взялась строка, из левого, правого, или обоих DataFrames. Колонка _merge
может оказаться очень полезной при работе с большими наборами данных для проверки корректности слияния.left = pd.DataFrame({"key": ["key1", "key2", "key3", "key4"], "value_l": [1, 2, 3, 4]})
right = pd.DataFrame({"key": ["key3", "key2", "key1", "key6"], "value_r": [3, 2, 1, 6]})
df_merge = left.merge(right, on='key', how='left', indicator=True)
Колонку
_merge
можно использовать, чтобы проверить, правильное ли количество строк с данными было взято из обоих DataFrames.df_merge._merge.value_counts()
both 3
left_only 1
right_only 0
Name: _merge, dtype: int64
3. Слияние по ближайшему значению [Nearest merge]
При работе с финансовыми данными, вроде криптовалют и ценных бумаг, бывает необходимо сопоставить котировки (изменения цен) со сделками. Скажем, мы хотим объединить каждую сделку с котировкой, которая обновилась за несколько миллисекунд до сделки. В pandas есть функция
merge_asof
, благодаря которой можно объединять DataFrames по ближайшему значению ключа (timestamp
в нашем случае). Наборы данных с котировками и сделками взяты из примера pandas.DataFrame
quotes
(«котировки») содержит изменения цен для разных акций. Как правило, котировок намного больше, чем сделок.quotes = pd.DataFrame(
[
["2016-05-25 13:30:00.023", "GOOG", 720.50, 720.93],
["2016-05-25 13:30:00.023", "MSFT", 51.95, 51.96],
["2016-05-25 13:30:00.030", "MSFT", 51.97, 51.98],
["2016-05-25 13:30:00.041", "MSFT", 51.99, 52.00],
["2016-05-25 13:30:00.048", "GOOG", 720.50, 720.93],
["2016-05-25 13:30:00.049", "AAPL", 97.99, 98.01],
["2016-05-25 13:30:00.072", "GOOG", 720.50, 720.88],
["2016-05-25 13:30:00.075", "MSFT", 52.01, 52.03],
],
columns=["timestamp", "ticker", "bid", "ask"],
)
quotes['timestamp'] = pd.to_datetime(quotes['timestamp'])
DataFrame
trades
содержит сделки по разным акциям.trades = pd.DataFrame(
[
["2016-05-25 13:30:00.023", "MSFT", 51.95, 75],
["2016-05-25 13:30:00.038", "MSFT", 51.95, 155],
["2016-05-25 13:30:00.048", "GOOG", 720.77, 100],
["2016-05-25 13:30:00.048", "GOOG", 720.92, 100],
["2016-05-25 13:30:00.048", "AAPL", 98.00, 100],
],
columns=["timestamp", "ticker", "price", "quantity"],
)
trades['timestamp'] = pd.to_datetime(trades['timestamp'])
Мы сливаем сделки и котировки по тикерам (котируемый инструмент, например акции), с условием, что
timestamp
последней котировки может быть на 10 мс меньше сделки. Если котировка появилась раньше сделки на более, чем 10 мс, бид (цена, которую готов заплатить покупатель) и аск (цена, по которой продавец готов продать) для этой котировки будут null
(тикер AAPL в этом примере).pd.merge_asof(trades, quotes, on="timestamp", by='ticker', tolerance=pd.Timedelta('10ms'), direction='backward')
4. Создание Excel-отчёта
Pandas (c библиотекой XlsxWriter) позволяет создать Excel-отчёт из DataFrame. Так можно сэкономить кучу времени — никакого больше экспорта DataFrame в CSV и ручного форматирования в Excel. Доступны также все виды диаграмм и т.д.
df = pd.DataFrame(pd.np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]), columns=["a", "b", "c"])
Фрагмент кода ниже создаёт таблицу в формате Excel. Чтобы сохранить её в файл, раскомментируйте строку
writer.save()
.report_name = 'example_report.xlsx'
sheet_name = 'Sheet1'
writer = pd.ExcelWriter(report_name, engine='xlsxwriter')
df.to_excel(writer, sheet_name=sheet_name, index=False)
# writer.save()
Как уже упоминалось ранее, с помощью библиотеки также можно добавлять диаграммы в отчёт. Нужно задать тип диаграммы (линейная в нашем примере) и диапазон данных для неё (диапазон данных должен быть в Excel-таблице).
# определяем книгу (workbook)
workbook = writer.book
worksheet = writer.sheets[sheet_name]
# создаём линейную диаграмму
chart = workbook.add_chart({'type': 'line'})
# настраиваем диапазон данных из таблицы для диаграммы,
# используя список значений вместо формул категория/значение:
# [sheetname, first_row, first_col, last_row, last_col]
chart.add_series({
'categories': [sheet_name, 1, 0, 3, 0],
'values': [sheet_name, 1, 1, 3, 1],
})
# настраиваем оси диаграммы
chart.set_x_axis({'name': 'Index', 'position_axis': 'on_tick'})
chart.set_y_axis({'name': 'Value', 'major_gridlines': {'visible': False}})
# размещаем диаграмму на листе
worksheet.insert_chart('E2', chart)
# сохраняем файл
writer.save()
5. Экономия места на диске
Работа над большим количеством проектов по анализу данных обычно оставляет след в виде большого количества обработанных данных из разных экспериментов. SSD на ноутбуке заполняется довольно быстро. Pandas позволяет сжимать данные при сохранении данных на диск и потом снова читать их уже из сжатого формата.
Создадим большой DataFrame со случайными числами.
df = pd.DataFrame(pd.np.random.randn(50000,300))
Если сохранить его как CSV, файл займет почти 300 МБ на жёстком диске.
df.to_csv('random_data.csv', index=False)
Один аргумент
compression='gzip'
уменьшает размер файла до 136 МБ.df.to_csv('random_data.gz', compression='gzip', index=False)
Сжатый файл читается тем же способом, что и обычный, так что никакой функциональности мы не теряем.
df = pd.read_csv('random_data.gz')
Заключение
Эти небольшие хитрости повысили продуктивность моей ежедневной работы с pandas. Надеюсь, из этой статьи вы узнали о какой-нибудь полезной функции, которая вам тоже поможет стать более продуктивным.
А какой ваш любимый приём в работе с pandas?
Matshishkapeu
Для экономии места на диске вместо зипования csv можно сохранять в parquet, по моим наблюдениям сжатие и скорость чтения обычно лучше. Для работы с данными превышающими объем памяти пандас умеет загружать их небольшими кусками, альтернативно можно использовать для этих целей Dask, с максимально близким к пандасу интерфейсом. Я недавно писал об этом небольшую статью https://habr.com/ru/post/488594/