В прошлом году мы начали публиковать данные в каталоге «Если быть точным» в формате 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: