Parquet en production : comment lire, écrire et optimiser les données pour l'analytique
Parquet est bien plus qu'un simple format de stockage : c'est un outil d'optimisation architecturale. Sa structure colonne, son filtrage intégré au moment de la lecture, son encodage par dictionnaire et ses métadonnées de groupe de lignes réduisent l'I/O de plusieurs ordres de grandeur et diminuent la taille des données jusqu'à 140 fois par rapport au CSV. Pour les ingénieurs et analystes de données, cela signifie non seulement économiser de l'espace disque, mais aussi réduire la latence des requêtes interactives, alléger la pression sur la mémoire et obtenir des performances prévisibles. Cet article fournit des conseils pratiques pour travailler avec Parquet en Python à un niveau intermédiaire ou avancé — depuis la lecture basique via pandas jusqu'au contrôle manuel du schéma, du partitionnement et de la compression.
Pourquoi Parquet remplace le CSV dans les pipelines de données modernes
Le CSV est un format textuel dépourvu de sémantique : chaque ligne est traitée comme un bloc unique, les délimiteurs sont analysés à la volée et les types sont inférés heuristiquement. Par contre, Parquet est binaire, strictement typé et colonne, avec des métadonnées intégrées. Les principales différences incluent :
- Stockage en colonnes : chaque colonne est stockée séparément, ce qui permet de ne lire que les champs nécessaires (par exemple,
yearetindicator_value) tout en ignorant le reste — sans avoir à analyser toute la ligne. - Groupes de lignes : les données sont divisées en blocs (par défaut ~128 Mo), chacun contenant des statistiques (
min,max,null_count). Lorsqu'on filtreyear == 2023, le moteur saute des groupes de lignes entiers sans les charger en mémoire. - Encodage par dictionnaire : pour les colonnes de chaînes à forte répétition (régions, catégories), Parquet stocke automatiquement ou explicitement un dictionnaire des valeurs uniques et des références vers celles-ci — ainsi, au lieu de « Région de Moscou » × 500 000 enregistrements, il utilise le code
1× 500 000. - Sécurité des types : dans le CSV, une valeur
NULLdans un champ numérique transforme toute la colonne enfloat64; dans Parquet, les types sont rigoureusement définis dans le schéma et validés lors de l'écriture.
La différence en pratique est énorme : un ensemble de données sur l'incidence du cancer de 576 Mo se réduit à 4 Mo en Parquet (rapport de compression ×144), et le temps de lecture d'un sous-ensemble filtré passe de 12–18 secondes à moins de 300 ms.
Lecture : de pandas au contrôle fin avec pyarrow
Lecture basique via pandas
Pour commencer rapidement, pd.read_parquet() suffit :
import pandas as pd
df = pd.read_parquet(
"data_zis_109_v20260126.parquet",
columns=["object_name", "year", "indicator_value"],
filters=[("year", "=", 2023)]
)
Il est important de noter que le filtrage se fait au moment de la lecture, et non après le chargement complet du DataFrame — ce qui est crucial pour les fichiers de plus de 1 Go.
Accès direct via pyarrow : contrôle du schéma et des métadonnées
Lorsqu'un contrôle précis est nécessaire, utilisez directement pyarrow.parquet :
import pyarrow.parquet as pq
# Sans charger les données — juste le schéma et les métadonnées
schema = pq.read_schema("data_zis_109_v20260126.parquet")
meta = pq.read_metadata("data_zis_109_v20260126.parquet")
print(f"Lignes : {meta.num_rows:,}, Colonnes : {meta.num_columns}")
# Lecture avec filtrage et sélection de colonnes
table = pq.read_table(
"data_zis_109_v20260126.parquet",
columns=["object_name", "object_oktmo", "year", "indicator_value"],
filters=[("year", "=", 2023)]
)
df = table.to_pandas() # Convertir en pandas si nécessaire
Pour les fichiers extrêmement volumineux, il est recommandé de lire en flux par lots :
pf = pq.ParquetFile("data_zis_109_v20260126.parquet")
for batch in pf.iter_batches(batch_size=100_000):
df_batch = batch.to_pandas()
# Traiter le lot
Écriture : de la simple sérialisation à la sérialisation de production
Les défis de la sérialisation automatique
df.to_parquet() fonctionne, mais cache des risques :
- Détermination implicite des types :
int64avec NULL →float64,datetime64→string. - Absence d'encodage par dictionnaire pour les colonnes de chaînes à très haute cardinalité.
- Taille inappropriée des groupes de lignes (par défaut 64 Mo), ce qui réduit l'efficacité du filtrage.
Schéma explicite et encodage contrôlé
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
)
Pourquoi zstd ? La compression est 30–50 % meilleure que snappy, avec une vitesse de décompression comparable. Elle offre le meilleur équilibre pour les charges de travail analytiques.
Partitionnement et travail avec des ensembles de fichiers
Si les données sont logiquement divisées (par année, région), utilisez write_to_dataset() :
pq.write_to_dataset(
table,
root_path="data_zis_partitioned/",
partition_cols=["year"]
)
La lecture devient alors efficace :
import pyarrow.dataset as ds
dataset = ds.dataset("data_zis_partitioned/", format="parquet", partitioning="hive")
table = dataset.to_table(filter=ds.field("year") == 2023)
Le partitionnement est justifié pour les ensembles de données de plus de 1 Go et lorsque la clé de partition compte moins de 1 000 valeurs distinctes. Évitez le partitionnement par des champs à haute cardinalité (par exemple, indicator_value) — cela crée des milliers de petits fichiers et nuit aux performances.
Ce qui compte
- Parquet ne remplace pas une base de données : il est idéal pour les scénarios d'analytique en mode append-only et fortement axés sur la lecture, mais ne supporte ni les transactions, ni les UPDATEs, ni les index.
- L'encodage par dictionnaire apporte des bénéfices maximaux lorsque la cardinalité est inférieure à 10⁴ et que la répétition dépasse 30 %. Pour les colonnes avec des valeurs uniques (UUIDs, timestamps), il est inutile.
- La taille du groupe de lignes influence la précision du filtrage : trop petite entraîne un excès de métadonnées ; trop grande rend le saut des données inutiles inefficace. La plage optimale est de 100 000 à 500 000 lignes.
- Les statistiques (
write_statistics=True) sont obligatoires en production. Sans elles, le filtrage se comporte comme une lecture complète. - Vérifiez toujours le schéma avant d'écrire :
pa.Table.from_pandas(df, schema=schema)lancera une erreur si les types ne correspondent pas — cela protège contre la corruption silencieuse.
— Editorial Team
Aucun commentaire pour le moment.