Parquet w produkcji: jak czytać, zapisywać i optymalizować dane do analizy
Parquet to nie tylko format przechowywania, ale również narzędzie do architektonicznej optymalizacji. Jego kolumnowa struktura, wbudowana filtracja na poziomie odczytu, kodowanie słownikowe oraz metadane grupy wierszy pozwalają zmniejszyć I/O nawet dziesięciokrotnie i obniżyć objętość danych aż do 140 razy w porównaniu z CSV. Dla inżynierów danych i analityków oznacza to nie tylko oszczędność miejsca na dysku, ale także redukcję latencji przy interaktywnych zapytaniach, mniejsze obciążenie pamięci i bardziej przewidywalną wydajność. W tym artykule znajdziesz praktyczne rekomendacje dotyczące pracy z Parquetem w Pythonie na poziomie middle/senior: od podstawowego odczytu za pomocą pandas po ręczne zarządzanie schematem, partycjonowaniem i kompresją.
Dlaczego Parquet zastępuje CSV w nowoczesnych pipeline'ach danych
CSV to format tekstowy bez semantyki: każda linia jest przetwarzana jako jeden blok, separatorzy są parsowane na bieżąco, a typy wyjściowe są wydedukowane heurystycznie. Parquet natomiast jest formatem binarnym, ściśle typizowanym, kolumnowym z wbudowaną meta-informacją. Kluczowe różnice to:
- Kolumnowe przechowywanie: każda kolumna jest zapisywana oddzielnie, co umożliwia odczyt tylko potrzebnych pól (np.
yeariindicator_value), ignorując pozostałe — bez konieczności pełnego rozparowania całej linii. - Grupy wierszy: dane są podzielone na bloki (standardowo około 128 MB), z których każdy zawiera statystyki (
min,max,null_count). Przy filtrowaniuyear == 2023silnik pomija całe grupy wierszy, nie ładowając ich do pamięci. - Kodowanie słownikowe: dla kolumn tekstowych o wysokiej powtarzalności (regiony, kategorie) Parquet domyślnie lub wyraźnie zapisuje słownik unikalnych wartości i referencje do nich — zamiast „Moskwa” × 500 tys. zapisów — kod
1× 500 tys. - Bezpieczeństwo typów: w CSV
NULLw polu numerycznym zamienia całą kolumnę nafloat64, podczas gdy w Parquet typy są twardej definicji w schemacie i sprawdzane podczas zapisu.
Różnica w praktyce jest ogromna: zestaw danych o zachorowalności na rak o objętości 576 MB w CSV skraca się do 4 MB w Parquet (skrócenie ×144), a czas odczytu filtrowanego podzbioru spada z 12–18 sekund do <300 ms.
Odczyt: od pandas do precyzyjnej kontroli z pyarrow
Podstawowy odczyt za pomocą pandas
Dla szybkiego startu wystarczy pd.read_parquet():
import pandas as pd
df = pd.read_parquet(
"data_zis_109_v20260126.parquet",
columns=["object_name", "year", "indicator_value"],
filters=[("year", "=", 2023)]
)
Ważne: filtracja odbywa się podczas odczytu, a nie po wczytaniu całego DataFrame — to kluczowe dla plików >1 GB.
Bezpośredni dostęp przez pyarrow: kontrola nad schematem i metadanymi
Gdy potrzebna jest dokładna kontrola, użyj bezpośrednio pyarrow.parquet:
import pyarrow.parquet as pq
# Bez wczytywania danych — tylko schemat i metadane
schema = pq.read_schema("data_zis_109_v20260126.parquet")
meta = pq.read_metadata("data_zis_109_v20260126.parquet")
print(f"Liczba wierszy: {meta.num_rows:,}, Liczba kolumn: {meta.num_columns}")
# Odczyt z filtrowaniem i wyborem kolumn
table = pq.read_table(
"data_zis_109_v20260126.parquet",
columns=["object_name", "object_oktmo", "year", "indicator_value"],
filters=[("year", "=", 2023)]
)
df = table.to_pandas() # konwersja do pandas, jeśli potrzebna
Dla bardzo dużych plików — odczyt strumieniowy w partiach:
pf = pq.ParquetFile("data_zis_109_v20260126.parquet")
for batch in pf.iter_batches(batch_size=100_000):
df_batch = batch.to_pandas()
# przetworzenie porcji
Zapis: od prostego to_parquet() do serializacji na poziomie production
Problemy z automatyczną serializacją
df.to_parquet() działa, ale ukrywa pewne ryzyka:
- Niejawne definiowanie typów:
int64z NULL →float64,datetime64→string. - Brak kodowania słownikowego dla kolumn tekstowych o wysokiej kardynalności.
- Niewłaściwy rozmiar grupy wierszy (standardowo 64 MB), co obniża efektywność filtracji.
Wyraźny schemat i kontrolowane kodowanie
import pyarrow as pa
import pyarrow.parquet as pq
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())
])
table = pa.Table.from_pandas(df, schema=schema, preserve_index=False)
pq.write_table(
table,
"data_zis_new_version.parquet",
compression="zstd",
row_group_size=500_000,
version="2.6",
write_statistics=True
)
Dlaczego zstd? Kompresja o 30–50% lepsza niż snappy przy porównywalnej szybkości dekompresji. Dla analizy danych — optymalny balans.
Partycjonowanie i praca z zestawami plików
Jeśli dane logicznie są podzielone (wg lat, regionów), użyj write_to_dataset():
pq.write_to_dataset(
table,
root_path="data_zis_partitioned/",
partition_cols=["year"]
)
Odczyt wtedy staje się efektywny:
import pyarrow.dataset as ds
dataset = ds.dataset("data_zis_partitioned/", format="parquet", partitioning="hive")
table = dataset.to_table(filter=ds.field("year") == 2023)
Partycjonowanie jest uzasadnione przy objętości >1 GB i kardynalności klucza partycjonującego <1000 wartości. Unikaj partycjonowania wg pola o wysokiej kardynalności (np. indicator_value) — to tworzy tysiące małych plików i pogarsza wydajność.
Co jest ważne
- Parquet nie zastępuje bazy danych: jest optymalny dla scenariuszy analizy danych, gdzie dominuje dodawanie i odczyt, ale nie obsługuje transakcji, UPDATE ani indeksów.
- Kodowanie słownikowe daje maksymalną wydajność przy kardynalności <10⁴ i proporcji powtórzeń >30%. Dla kolumn z unikalnymi wartościami (UUID, timestampy) jest bezsensowne.
- Rozmiar grupy wierszy wpływa na dokładność filtracji: zbyt mały — dużo metadanych; zbyt duży — brak efektywnego pomijania niepotrzebnych danych. Optymalny rozmiar: 100–500 tys. wierszy.
- Statystyki (
write_statistics=True) są obowiązkowe w produkcji. Bez nich filtracja działa jak pełny scan. - Zawsze sprawdzaj schemat przed zapisem:
pa.Table.from_pandas(df, schema=schema)wygeneruje błąd przy niezgodności typów — to ochrona przed cichym uszkodzeniem.
— Editorial Team
Brak komentarzy.