Наши датасеты теперь можно скачать в формате Parquet. Рассказываем, в чем его плюсы и как с ним работать
Этот текст впервые вышел в рассылке «Это не показатель». Каждую неделю мы публикуем инструкции, загадки, разборы графиков и другие полезные материалы для тех, кто работает с данными. Присоединяйтесь через Tribute или Boosty.
Зачем нужен этот формат
Одно из первых отличий, которое вы заметите — размер файлов: Parquet весит сильно меньше, чем тот же CSV. Например, датасет по заболеваемости раком в формате CSV занимает 576 Мб, а в Parquet — всего 4 Мб — в 144 раза меньше! Другое полезное свойство — возможность отфильтровать данные сразу при чтении, например, прочитать данные о заболеваемости только за 2024 год.
CSV, к которому все привыкли, хранит таблицу как обычный текст — строка за строкой, где в каждой записи подряд записаны все колонки. При чтении программе приходится просматривать файл целиком и разбирать текст: искать разделители, читать строки и превращать числа из текста в значения. Parquet устроен по-другому: в нем каждая колонка лежит отдельно, причем данные разбиты на крупные блоки строк со статистикой значений. Если нужны только несколько полей, читаются только они, а блоки, которые не подходят под условия запроса, даже не загружаются с диска.
В колонках Parquet дополнительно уменьшает объем данных. Например, если в поле региона много раз повторяются «Московская область», «Краснодарский край» и «Татарстан», формат может сохранить список уникальных значений, а в самих данных хранить короткие номера вместо длинных строк. Числа тоже записываются компактнее, чем в текстовом виде. В итоге вместо многократно повторяющихся слов получается небольшой набор кодов.
Быстрый старт
Покажем, как работать с форматом Parquet, на примере датасета о заболеваемости раком.
Установите нужные пакеты, скачайте архив с данными и распакуйте его.
# pip
pip install pyarrow pandas duckdb
# uv
uv pip install pyarrow pandas duckdb
# conda
conda install -c conda-forge pyarrow pandas duckdb
# poetry
poetry add pyarrow pandas duckdb
Самый простой способ прочитать parquet-файл — использовать хорошо знакомый pandas. Можно прочитать все колонки, а можно — только нужные.
import pandas as pd
# Читаем весь файл
df = pd.read_parquet("data_zis_109_v20260126.parquet")
# Читаем только нужные колонки (быстрее и экономит память)
df = pd.read_parquet("data_zis_109_v20260126.parquet", columns=["object_name", "object_level", "object_oktmo", "object_okato", "year", "indicator_value"])
В итоге получаете обычный DataFrame и работаете с ним в привычном формате. Для большинства задач этого достаточно — pd.read_parquet() поддерживает и выбор колонок, и фильтрацию по строкам, которая применяется уже при чтении, а не после загрузки всего датасета в память.
df = pd.read_parquet(
"data_zis_109_v20260126.parquet",
columns=["object_name", "object_oktmo", "year", "indicator_value"],
filters=[("year", "=", 2023)]
)
Использование pyarrow
pyarrow — это библиотека, которая лежит в основе работы с Parquet в Python. Когда вы вызываете pd.read_parquet(), pandas под капотом использует именно ее. Но если обращаться к pyarrow напрямую, появляется больше контроля над тем, как именно читаются данные.
Первое, что стоит сделать с новым файлом — заглянуть в его структуру, не загружая сами данные. Для этого у файлов Parquet есть схема. В схеме хранятся названия колонок и их типы.
import pyarrow.parquet as pq
# Смотрим названия колонок и их типы — данные не загружаются
schema = pq.read_schema("data_zis_109_v20260126.parquet")
print(schema)
# Смотрим сколько строк и колонок в файле
meta = pq.read_metadata("data_zis_109_v20260126.parquet")
print(f"Строк: {meta.num_rows:,}")
print(f"Колонок: {meta.num_columns}")
Теперь читаем данные. Здесь можно указать фильтр, и pyarrow применит его еще в процессе чтения, не загружая лишнее.
# Читаем только данные за 2023 год — остальное даже не загружается
table = pq.read_table(
"data_zis_109_v20260126.parquet",
columns=["object_name", "object_oktmo", "year", "indicator_value"],
filters=[("year", "=", 2023)]
)
Результат — Arrow Table — это не совсем привычный DataFrame, но выглядит похоже. Если хотите продолжать работать в pandas — нужна всего одна строчка.
df = table.to_pandas()
Если файл настолько большой, что даже частичная загрузка перегружает память, можно читать его небольшими кусками — по 100 000 строк за раз.
pf = pq.ParquetFile("data_zis_109_v20260126.parquet")
for batch in pf.iter_batches(batch_size=100_000):
df = batch.to_pandas()
# обрабатываем каждую часть отдельно
Как сохранить данные в формате Parquet
Сохранить pandas DataFrame очень просто (про параметр compression еще поговорим).
df.to_parquet("data_zis_new_version.parquet", index=False, compression="zstd")
Этого достаточно для простых случаев. Но если файл будет использоваться другими людьми или другими системами, стоит подойти к сохранению чуть внимательнее. Хорошая практика — задавать схему данных — для каждой колонки явно определять ее тип.
Когда вы сохраняете DataFrame без указания схемы, pyarrow сам угадывает типы колонок — и иногда промахивается. Самый частый пример: если в числовой колонке есть хотя бы одно пустое значение, pandas хранит ее как float64 вместо int64. В итоге в файле оказываются числа с десятичной точкой там, где их быть не должно. Или колонка с датами сохраняется как обычный текст, потому что pandas не распознал формат.
Явная схема решает эту проблему: вы сами говорите, какой тип должен быть у каждой колонки, и pyarrow проверит это при сохранении.
import pyarrow as pa
import pyarrow.parquet as pq
# Описываем схему: имя колонки и ее тип
schema = pa.schema([
pa.field("object_name", pa.string()),
pa.field("object_oktmo", pa.string()),
pa.field("year", pa.int32()),
pa.field("indicator_value", pa.float64()),
])
# Конвертируем DataFrame в Arrow Table с указанной схемой
table = pa.Table.from_pandas(df, schema=schema, preserve_index=False)
# Сохраняем
pq.write_table(table, "data_zis_new_version.parquet")
Если данные не соответствуют схеме — например, в колонке year окажется текст — pyarrow сообщит об этом сразу, при сохранении, а не позже, когда кто-то попытается прочитать файл и получит неожиданный результат.
Явная схема полезна не только для корректности типов, но и для оптимизации хранения. Для колонок с повторяющимися строковыми значениями — регионы, коды заболеваний, категории — можно явно указать тип dictionary: тогда pyarrow сохранит список уникальных значений один раз, а в самих данных будет хранить короткие числовые коды вместо повторяющихся строк. Это один из главных инструментов сжатия в Parquet — помогает сильно уменьшать размер файла с данными.
schema = pa.schema([
pa.field("object_name", pa.string()),
# dictionary: вместо повторяющихся строк хранятся числовые коды
pa.field("object_oktmo", pa.dictionary(pa.int32(), pa.string())),
pa.field("year", pa.int32()),
pa.field("indicator_value", pa.float64()),
])
Первый аргумент pa.dictionary() — тип индекса (насколько длинным будет код), второй — тип самих значений. int32 подойдет, если уникальных значений меньше двух миллиардов — для регионов и кодов этого более чем достаточно. Если значений совсем мало (например, десяток категорий), можно использовать int8.
Parquet применяет dictionary encoding автоматически — но не всегда делает это эффективно. По умолчанию он включает его для колонки, если в первом row group доля уникальных значений достаточно мала. Если в начале файла данные оказались разнообразными, а дальше — повторяющимися, кодирование может не примениться вообще. Кроме того, pyarrow может отключить его на лету, если словарь становится слишком большим.
Есть еще несколько параметров, которые влияют на размер файла и последующую скорость чтения:
- compression — алгоритм сжатия, рекомендуем использовать zstd, файл получается заметно меньше, чем с другим алгоритмом snappy, а скорость чтения практически не страдает.
- row_group_size — размер одного блока данных внутри файла в строках. От этого зависит, насколько точно работает фильтрация при чтении: чем меньше блок, тем точнее можно пропустить ненужное, но тем больше служебных метаданных. Для большинства датасетов хорошо работает значение от 100 000 до 500 000 строк.
- write_statistics — сохранять ли статистику по каждому блоку: минимум, максимум и количество пустых значений по каждой колонке. Именно она позволяет при чтении пропускать блоки, которые заведомо не содержат нужных данных. По умолчанию включена.
pq.write_table(
table,
"data_zis_new_version.parquet",
# Сжатие. zstd — лучший выбор: файл получается
# примерно в полтора раза меньше, чем с snappy (другой алгоритм сжатия),
# а читается почти так же быстро
compression="zstd",
# Размер блока внутри файла. От этого зависит,
# насколько точно работает фильтрация при чтении.
# Значение до 500 000 строк подходит для большинства датасетов
row_group_size=500_000,
# Версия формата. 2.6 — современная,
# поддерживает все актуальные типы данных
version="2.6",
# Статистика по колонкам — нужна для быстрой
# фильтрации при чтении, лучше не отключать
write_statistics=True,
)
Бывает, что данные разбиты по отдельным файлам — например, каждый год или каждый регион лежит в своем Parquet-файле. Собирать их вручную через цикл и pd.concat() не нужно — pyarrow Dataset умеет читать папку с файлами как единую таблицу.
import pyarrow.dataset as ds
# Читаем все parquet-файлы из папки
dataset = ds.dataset("data/", format="parquet")
# Можно сразу применить фильтр и выбрать колонки
df = dataset.to_table(
columns=["object_name", "year", "indicator_value"],
filter=ds.field("year") > 2020
).to_pandas()
Если файлы лежат не в одной папке, можно передать список путей до них явно:
dataset = ds.dataset(
["data/2021.parquet", "data/2022.parquet", "data/2023.parquet"],
format="parquet"
)
Если датасет большой и вы знаете, что чаще всего будете фильтровать по какой-то одной колонке — например, по году или региону, — имеет смысл сохранить файл не целиком, а разбить на части. Эта операция называется партиционированием. Вместо одного большого файла на диске появится папка, внутри которой данные разложены по подпапкам.
import pyarrow.dataset as ds
pq.write_to_dataset(
table,
root_path="data_zis_new_version/",
partition_cols=["year"],
)
Когда вы потом читаете такой датасет с фильтром по году — загружается только нужная подпапка, остальные не используются.
import pyarrow.dataset as ds
dataset = ds.dataset("data_zis_new_version/", format="parquet", partitioning="hive")
table = dataset.to_table(filter=ds.field("year") == 2023)
df = table.to_pandas()
Партиционировать стоит только если датасет весит от 1 ГБ и есть четкий паттерн фильтрации. Для небольших файлов это лишнее усложнение. И важно не выбирать колонку с очень большим количеством уникальных значений — например, партиционирование по значению показателя создаст тысячи крошечных файлов, что только замедлит работу.
Еще одна полезная возможность — сохранять вместе с файлом произвольные метаданные: откуда данные, кто их подготовил, какая версия и любые другие. Это особенно удобно для файлов в каталоге данных — можно понять контекст, не загружая сам файл.
import json
metadata = {
"source": "Ежегодники «Злокачественные новообразования в России (заболеваемость и смертность)»",
"dataset": "Заболеваемость онкологией и смертность от нее с 1997 года",
"version": "20260126",
}
schema = pa.schema([
pa.field("object_name", pa.string()),
pa.field("object_oktmo", pa.dictionary(pa.int32(), pa.string())),
pa.field("year", pa.int32()),
pa.field("indicator_value", pa.float64()),
]).with_metadata({
"custom": json.dumps(metadata, ensure_ascii=False)
})
table = pa.Table.from_pandas(df, schema=schema, preserve_index=False)
pq.write_table(table, "data_zis_new_version.parquet", compression="zstd")
Прочитать метаданные можно без загрузки самих данных:
schema = pq.read_schema("data_zis_new_version.parquet")
meta = json.loads(schema.metadata[b"custom"])
print(meta["version"]) # 20260126
Использование fastparquet
Помимо pyarrow, есть еще одна библиотека для работы с Parquet — fastparquet. Она менее распространена, но иногда встречается в старых проектах. Использовать ее можно прямо через pandas.
# Чтение
df = pd.read_parquet("data_zis_109_v20260126.parquet", engine="fastparquet")
# Запись
df.to_parquet("data_zis_new_version.parquet", engine="fastparquet")
Одна особенность, которая отличает fastparquet от pyarrow, — возможность дописывать данные в существующий файл.
df_new.to_parquet("data_zis_109_v20260126.parquet", engine="fastparquet", append=True)
Схема нового DataFrame должна точно совпадать со схемой существующего файла. Если схемы расходятся, файл молча повреждается или дописывается некорректно.
Для большинства задач pyarrow — первый выбор. fastparquet может пригодиться только если вам нужна дозапись в файл и нет возможности использовать другой подход.
Чек-лист по работе с PARQUET
Чтение:
-
Заглянуть в схему перед загрузкой — pq.read_schema() покажет колонки и типы без загрузки данных
-
Читать только нужные колонки — передавать список в параметр columns=
-
Использовать фильтры при чтении через pyarrow — filters=[("year", "=", 2023)] — тогда лишние данные даже не загружаются с диска
Сохранение:
-
Задать схему явно через pa.schema() — иначе pyarrow будет угадывать типы и может ошибиться (например, int64 → float64 при наличии пустых значений)
-
Выбрать алгоритм сжатия: рекомендуется zstd
-
Сохранять метаданные вместе с файлом через schema.with_metadata()
-
Партиционировать по колонке с небольшим числом уникальных значений (год, регион — да, значение показателя — нет)
Что еще почитать
В следующей инструкции мы расскажем, как использование библиотек polars и duckdb еще увеличивает эффективность работы с большими файлами. А пока можно почитать дополнительные материалы про формат Parquet:
-
Anatomy of a Parquet File от Towards Data Science — чтобы разобраться, почему Parquet — такой эффективный формат хранения
-
Паркет: потрогаем parquet файл руками на Хабр — про внутреннее устройство формата
-
Видео, которое поможет разобраться, как устроены Parquet-файлы
