Feather Format
История: зачем Feather
В 2016 году Wes McKinney (создатель pandas) и Hadley Wickham (автор tidyverse в R) столкнулись с одной проблемой: передача DataFrame между Python и R требовала CSV-сериализации — медленной, с потерей типов, и неуправляемой для больших данных. Они создали Feather — бинарный формат для быстрого обмена табличными данными между языками.
Feather v1 использовал собственную FlatBuffers-схему для метаданных, но хранил данные в Arrow-колоночном формате. Это был proof of concept — быстро, но ограниченно.
Feather — это не конкурент Parquet. Feather оптимизирован для скорости чтения/записи (временное хранение, кэш, обмен между процессами). Parquet — для компактного долговременного хранения с rich metadata и predicate pushdown.
Feather v1 vs v2
Feather v1 (2016)
Feather v1 (2016): собственная FlatBuffers-схема, ограниченный набор типов, без компрессии. Максимум 2 ГБ на колонку из-за int32 offsets.Feather v2 (Arrow 0.17+)
Feather v2 (Arrow 0.17, 2020): идентичен Arrow IPC File Format. Поддерживает все Arrow-типы, LZ4/ZSTD компрессию, произвольный chunk size. Лимит на колонку снят.Ключевое решение Arrow-команды: вместо развития собственного формата Feather v1, они сделали v2 идентичным Arrow IPC File Format. Это означает:
- Файл
.feather(v2) — это валидный.arrowфайл - Любой Arrow-клиент (C++, Java, Rust, Go) может читать Feather v2 без специальной библиотеки
- Обратное тоже верно: файл, записанный через
pyarrow.ipc.new_file(), читается черезfeather.read_feather()
Структура файла на диске
Padding (2 bytes)
Padding до 8 байт после magic bytes. Обеспечивает выравнивание FlatBuffers Schema message.Благодаря Footer с Block descriptors, Feather v2 поддерживает random access: можно прочитать batch #42 без сканирования предыдущих 41 — seek по offset из footer.
Компрессия
Feather v2 поддерживает покадровую (per-buffer) компрессию:
| Кодек | Характеристика | Когда использовать |
|---|---|---|
| LZ4 (по умолчанию) | Быстрая декомпрессия, умеренное сжатие | Кэш, промежуточные файлы, IPC |
| ZSTD | Лучшее сжатие, медленнее LZ4 | Архивация, сетевая передача |
| uncompressed | Максимальная скорость чтения | mmap, zero-copy сценарии |
Компрессия в Feather/IPC — это trade-off: сжатые буферы нельзя использовать zero-copy, потому что данные нужно декомпрессировать в новый буфер. Если latency чтения критична и диск быстрый — пишите без компрессии.
pyarrow.feather API
Запись:
import pyarrow as pa
import pyarrow.feather as feather
table = pa.table({
"user_id": pa.array([1, 2, 3, 4, 5]),
"name": pa.array(["Alice", "Bob", "Charlie", "Diana", "Eve"]),
"score": pa.array([95.5, 87.3, None, 91.0, 78.8]),
})
# LZ4 по умолчанию
feather.write_feather(table, "users.feather")
# ZSTD с уровнем 3
feather.write_feather(table, "users_zstd.feather",
compression="zstd",
compression_level=3)
# Без компрессии — для mmap
feather.write_feather(table, "users_raw.feather",
compression="uncompressed")
# Feather v1 (legacy, для совместимости со старыми ридерами)
feather.write_feather(table, "users_v1.feather", version=1)
Чтение:
# Как pandas DataFrame
df = feather.read_feather("users.feather")
# Как pyarrow Table (без конвертации в pandas)
table = feather.read_table("users.feather")
# Выборочное чтение колонок
df_names = feather.read_feather("users.feather",
columns=["user_id", "name"])
# Memory-mapped чтение (zero-copy если без компрессии)
table_mm = feather.read_table("users_raw.feather",
memory_map=True)
read_table() возвращает pyarrow.Table — Arrow-нативный объект. read_feather() — pandas.DataFrame. Если дальше данные идут в Arrow-совместимый движок (DuckDB, Polars), используйте read_table() чтобы избежать лишней конвертации.
Feather vs Parquet: когда что
Feather
Feather оптимален когда важна скорость I/O. Данные хранятся в Arrow-формате — минимум трансформаций при чтении/записи.Parquet
Parquet оптимален для долговременного хранения. Мощные кодировки (RLE, DICT, DELTA), rich metadata, row group pruning, Bloom-фильтры.Правило: Feather — для скорости, Parquet — для хранения.
Числа (приблизительные, зависят от данных и железа):
- Запись: Feather 2-5× быстрее Parquet (нет encoding stage)
- Чтение: Feather 1.5-3× быстрее Parquet (нет decode stage)
- Размер: Parquet 2-10× компактнее (RLE + dictionary + delta encoding перед компрессией)
import pyarrow as pa
import pyarrow.feather as feather
import pyarrow.parquet as pq
table = pa.table({"x": range(1_000_000), "y": ["cat", "dog"] * 500_000})
# Feather: быстро, но больше
feather.write_feather(table, "data.feather") # ~12 MB, 50ms
# Parquet: медленнее, но компактнее
pq.write_table(table, "data.parquet") # ~2 MB, 200ms
DuckDB и Polars с Feather
Feather v2 — first-class citizen в Arrow-нативных движках:
import duckdb
# DuckDB читает Feather напрямую через Arrow
result = duckdb.sql("""
SELECT name, AVG(score) as avg_score
FROM read_arrow('users.feather')
GROUP BY name
""").fetchdf()
import polars as pl
# Polars: scan_ipc для lazy evaluation
lf = pl.scan_ipc("users.feather")
result = lf.filter(pl.col("score") > 80).collect()
Polars использует scan_ipc() / read_ipc() — потому что Feather v2 = IPC File. Название функции отражает реальный формат, а не историческое имя.
Ключевые выводы
- Feather v1 (2016) — proof of concept от McKinney и Wickham: собственная схема, ограниченные типы, без компрессии, лимит 2 ГБ/колонка
- Feather v2 (Arrow 0.17, 2020) — идентичен Arrow IPC File Format. Все Arrow-типы, LZ4/ZSTD компрессия, нет лимитов
- Структура — ARROW1 magic + Schema + DictionaryBatch + RecordBatch + Footer + ARROW1. Random access через Block descriptors
- Компрессия — LZ4 (по умолчанию), ZSTD, uncompressed. Компрессия убивает zero-copy — trade-off скорость/размер
- Feather vs Parquet — Feather для скорости (кэш, IPC, обмен), Parquet для хранения (encoding, pushdown, компактность)
- API —
feather.write_feather()/feather.read_table(). DuckDB и Polars читают Feather нативно как Arrow IPC