В прошлом году мы начали публиковать данные в каталоге «Если быть точным» в формате Parquet. Его придумали инженеры Twitter и Cloudera в 2013 году, и сегодня он стал стандартом хранения аналитических данных — его используют Google, Amazon, Netflix и большинство современных data-платформ.
В этом гайде мы расскажем, как эффективно работать с данными в формате Parquet с помощью Python.
Про Если быть точным
«Если быть точным» — это платформа с открытыми данными и исследованиями по социальным проблемам в регионах России. Мы собираем данные и публикуем исследования по более чем 20 социальным проблемам в России. В нашем каталоге уже опубликовано больше 40 наборов данных.
Зачем нужен этот формат
Одно из первых отличий, которое вы заметите — размер файлов: 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-файлы
