Наши датасеты теперь можно скачать в формате Parquet. Рассказываем, в чем его плюсы и как с ним работать

В прошлом году мы начали публиковать данные в каталоге «Если быть точным» в формате Parquet. Его придумали инженеры Twitter и Cloudera в 2013 году, и сегодня он стал стандартом хранения аналитических данных — его используют Google, Amazon, Netflix и большинство современных data-платформ. В этом гайде мы расскажем, как эффективно работать с данными в формате Parquet с помощью Python.
> > > >

Этот текст впервые вышел в рассылке «Это не показатель». Каждую неделю мы публикуем инструкции, загадки, разборы графиков и другие полезные материалы для тех, кто работает с данными. Присоединяйтесь через 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:

Материал был полезен?

«Если быть точным» — это данные с человеческим лицом.
Поддержите нас, чтобы мы могли и дальше помогать решать социальные проблемы.
Мы всегда рады вашим письмам
Присылайте ваши вопросы, отклики и предложения в телеграм-бот @tochno_bot
Наши соцсети