nom | profession | |
---|---|---|
0 | Astérix | |
1 | Obélix | Tailleur de menhir |
2 | Assurancetourix | Barde |
Nous avons vu dans les chapitres précédents comment récupérer, et harmoniser, des données issues de multiples sources: fichiers type CSV, API, webscraping, etc. Le panorama des manières possibles de consommer de la donnée serait incomplet sans évoquer un nouveau venu dans le paysage de la donnée, à savoir le format de données Parquet
. Du fait de ses caractéristiques techniques pensées pour l’analyse de données, et de sa simplicité d’usage avec Python
, ce format devient de plus en plus incontournable. Il s’agit d’ailleurs d’une pierre angulaire des infrastructures cloud qui, depuis le milieu des années 2010, tendent à devenir l’environnement usuel dans le domaine de la data science (pour plus de détails, voir le cours de mise en production de Romain Avouac et moi).
- Comprendre les enjeux liés au stockage et au traitement de différents formats de données ;
- Distinguer le stockage sous forme de fichier et sous forme de base de données ;
- Découvrir le format
Parquet
, ses avantages par rapport aux formats plats ou propriétaires ;
- Apprendre à traiter ces données avec
Arrow
etDuckDB
;
- Identifier les implications du stockage dans le cloud et comment
Python
peut s’y adapter.
Ce chapitre s’appuie sur un atelier dédié au sujet que j’ai donné dans le cadre du réseau des data scientists de la statistique publique (SSPHub
)
Afficher les slides associées
Regarder le _replay_ de la session _live_ du 09 Avril 2025:
1 Elements de contexte
1.1 Principe du stockage de la donnée
Avant de comprendre les apports du format Parquet
, il est utile de revenir brièvement sur la manière dont l’information est stockée et rendue accessible à un langage de traitement comme Python
1.
Deux approches principales coexistent : le stockage sous forme de fichiers et celui sous forme de bases de données relationnelles. La distinction entre ces deux paradigmes repose sur la façon dont l’accès aux données est organisé.
1.2 Le stockage sous forme de fichiers
1.2.1 Les fichiers plats
Dans un fichier plat, les données sont organisées de manière linéaire, souvent séparées par un caractère (virgule, point-virgule, tabulation). Exemple avec un fichier .csv
:
nom ; profession
Astérix ;
Obélix ; Tailleur de menhir ;
Assurancetourix ; Barde
Python
peut facilement structurer cette information :
StringIO
permet de traiter la chaîne de caractère comme le contenu d’un fichier.
À propos des fichiers de ce type, on parle de fichiers plats car les enregistrements relatifs à une observation sont stockés ensemble, sans hiérarchie.
1.2.2 Les fichiers hiérarchiques
D’autres formats, comme JSON
, structurent les données de manière hiérarchique :
[
{
"nom": "Astérix"
},
{
"nom": "Obélix",
"profession": "Tailleur de menhir"
},
{
"nom": "Assurancetourix",
"profession": "Barde"
}
]
Cette fois, quand on n’a pas d’information, on ne se retrouve pas avec nos deux séparateurs accolés (cf. la ligne “Astérix”) mais l’information n’est tout simplement pas collectée.
La différence entre un fichier .csv
et un fichier JSON
ne réside pas seulement dans le format : elle implique une autre logique de stockage.
Le format JSON
, non tabulaire, est plus souple : il permet de mettre à jour la structure des données sans recompiler ou modifier les anciennes lignes. Cela facilite la collecte évolutive dans des contextes comme les API.
Par exemple, un site web qui collecte de nouvelles données n’aura pas à mettre à jour l’ensemble de ses enregistrements antérieurs pour stocker la nouvelle donnée (par exemple pour indiquer que pour tel ou tel client cette donnée n’a pas été collectée) mais pourra la stocker dans un nouvel item.
Ce sera à l’outil de requête (Python
ou un autre outil)
de créer une relation entre les enregistrements stockés à des endroits différents.
C’est ce principe qui sous-tend de nombreuses bases NoSQL
(comme ElasticSearch
), centrales dans l’univers du big data.
1.2.3 Données réparties sur plusieurs fichiers
Il est fréquent qu’une observation soit répartie entre plusieurs fichiers de formats différents. Par exemple, en géomatique, les contours géographiques peuvent être stockés de différentes manières pour accompagner les données qu’elles contextualisent:
- Soit tout est empilé dans un unique fichier qui contient à la fois les contours géographiques et les valeurs attributaires. Cette logique est celle suivie, par exemple, par le
GeoJSON
; - Soit plusieurs fichiers se répartissent l’ensemble des données et, pour lire la donnée dans son ensemble (contours géographiques, données des différentes zones géographiques, système de projection, etc.), il faudra donc associer ceux-ci pour avoir un tableau de données complet. C’est l’approche suivie par le format
Shapefile
.
Lorsque la donnée est éclatée dans plusieurs fichiers, c’est alors à l’outil de traitement (ex. Python
) d’effectuer la jonction logique.
1.2.4 Le rôle du file system
Le système de fichiers (file system) permet à l’ordinateur de localiser physiquement les fichiers sur le disque. C’est un composant central dans la gestion de fichiers : il assure leur nommage, leur hiérarchie et leur accès.
1.3 Le stockage sous forme de bases de données
La logique des bases de données est différente. Elle est plus systémique. Une base de données relationnelle est gérée par un Système de Gestion de Base de Données (SGBD) qui permet :
- de stocker des ensembles cohérents de données,
- d’en permettre la mise à jour (ajout, suppression, modification),
- d’en contrôler l’accès (droits utilisateurs, types de requêtes, etc.).
Les données sont organisées en tables reliées par des relations, souvent selon un schéma en étoile :
Le logiciel associé à la base de données fera ensuite le lien entre ces tables à partir de requêtes SQL
. L’un des logiciels les plus efficaces dans ce domaine est PostgreSQL
.
Python
est tout à fait utilisable pour passer une requête SQL à un gestionnaire de base de données. Historiquement, les packages sqlalchemy
et psycopg2
ont été très utilisés pour envoyer des requêtes à une base de données PostgreSQL
(lire celle-ci, la mettre à jour, etc.). Aujourd’hui, DuckDB
, sur lequel nous reviendrons lorsque nous parlerons du format Parquet
, est un choix pratique pour passer des requêtes SQL à un SGBD PostgreSQL
.
Pourquoi les fichiers ont le vent en poupe
Le succès croissant des fichiers dans l’écosystème de la data science s’explique par plusieurs facteurs techniques et pratiques qui les rendent particulièrement adaptés aux usages analytiques modernes.
En premier lieu, les fichiers sont beaucoup plus légers à manipuler que les bases de données. Ils ne nécessitent pas l’installation ou le maintien d’un logiciel de gestion spécialisé : un simple file system, déjà présent sur tout système d’exploitation, suffit à y accéder.
Pour lire un fichier dans Python
, il suffit d’utiliser une librairie comme Pandas
. À l’inverse, interagir avec une base de données implique souvent :
- l’installation et la configuration d’un SGBD (comme
PostgreSQL
,MySQL
, etc.) ; - la gestion d’une connexion réseau ;
- le recours à des bibliothèques comme
sqlalchemy
oupsycopg2
.
Cette différence de complexité rend l’approche fichier beaucoup plus souple et rapide pour les tâches exploratoires ou ponctuelles.
Cette légèreté a une contrepartie : les fichiers ne permettent pas une gestion fine des droits d’accès. Il est difficile, par exemple, d’empêcher un utilisateur de modifier ou supprimer la donnée à moins de dupliquer le fichier et de travailler sur une copie. C’est l’une des limites de l’approche fichier dans les environnements multi-utilisateurs mais auxquelles les solutions cloud, notamment la technologie S3
sur laquelle nous reviendrons, apportent des réponses.
La principale raison pour laquelle les fichiers sont souvent privilégiés par rapport aux SGBD réside dans la nature des opérations effectuées. Les bases de données relationnelles prennent tout leur sens lorsque l’on doit gérer des écritures fréquentes ou des mises à jour complexes sur des ensembles de données structurés — c’est-à-dire dans une logique applicative, où la donnée évolue continuellement (ajout, modification, suppression).
À l’inverse, dans un contexte analytique, on se contente généralement de lire et de manipuler temporairement des données sans modifier la source. L’objectif est d’interroger, d’agréger, de filtrer — pas de pérenniser les changements. Pour ce type d’usage, les fichiers (notamment dans des formats optimisés comme Parquet
, comme nous allons le voir) sont parfaitement adaptés : ils offrent une lecture rapide, une portabilité élevée et n’imposent pas l’intermédiation d’un moteur de base de données.
2 Le format Parquet
Le format CSV
a longtemps été plébiscité en raison de sa simplicité :
- Il est lisible par un humain (un simple éditeur de texte suffit pour en lire le contenu) ;
- Il repose sur une structure tabulaire simple, bien adaptée à de nombreuses situations d’analyse ;
- Il est universel et interopérable, car non dépendant d’un logiciel particulier.
Mais cette simplicité a un coût. Plusieurs limites du format CSV
ont justifié l’émergence de formats plus performants pour l’analyse de données comme Parquet
2.1 Limites du format CSV
Le CSV est un format lourd :
- Il n’est pas compressé, ce qui augmente sa taille disque ;
- Toutes les données y sont stockées de façon brute. L’optimisation du typage (entier, flottant, chaîne…) est laissée à la librairie qui l’importe (comme
Pandas
), ce qui nécessite de scanner les données à l’ouverture, augmentant le temps de chargement et le risque d’erreur.
Le CSV est orienté ligne :
- Pour accéder à une colonne spécifique, il faut lire chaque ligne du fichier puis en extraire la colonne d’intérêt ;
- Ce modèle est peu performant lorsqu’on souhaite ne manipuler qu’un sous-ensemble de colonnes — un cas très courant en data science.
Le CSV est coûteux à modifier :
Ajouter une colonne ou insérer une donnée intermédiaire implique de réécrire tout le fichier. Par exemple, ajouter une colonne
cheveux
nécessiterait de produire une nouvelle version du fichier :nom ; cheveux ; profession Astérix ; blond ; Obélix ; roux ; Tailleur de menhir Assurancetourix ; blond ; Barde
La plupart des outils de data science proposent des formats de sérialisation spécifiques :
.pickle
pourPython
,
.rda
ou.RData
pourR
,
.dta
pourStata
,
.sas7bdat
pourSAS
.
Cependant, ces formats sont propriétaires ou fortement couplés à un langage, ce qui pose des problèmes d’interopérabilité. Par exemple, Python
ne peut pas lire nativement un .sas7bdat
. Même s’il existe des bibliothèques dédiées, l’absence de documentation officielle rend le support incertain.
À ce titre, malgré ses limites, le .csv
conserve l’avantage de l’universalité. Mais le format Parquet
combine cette portabilité avec des performances bien supérieures.
2.2 L’émergence du format Parquet
Pour répondre à ces limites, le format Parquet
, développé comme projet open-source Apache, propose une approche radicalement différente.
Sa principale caractéristique : il est orienté colonne. Contrairement au CSV, les données de chaque colonne sont stockées séparément. Cela permet :
- de charger uniquement les colonnes utiles à une analyse ;
- de compresser plus efficacement les données ;
- d’accélérer significativement les requêtes sélectives.
Voici une représentation tirée du blog d’Upsolver qui illustre la différence entre stockage ligne (row-based
) et stockage colonne (columnar
) :
Dans notre exemple, on pourrait lire la colonne profession
sans parcourir les noms, ce qui rend l’accès plus rapide (ignorez l’élément pyarrow.Table
, nous
reviendrons dessus) :
pyarrow.Table
nom : string
profession: string
----
nom : [["Astérix ","Obélix ","Assurancetourix "]]
profession: [["","Tailleur de menhir","Barde"]]
Grâce à la structure orientée colonne, il est possible de lire uniquement une variable (comme profession
) sans avoir à parcourir toutes les lignes du fichier.
path
└── to
└── table
├── gender=male
│ ├── country=US
│ │ └── data.parquet
│ ├── country=CN
│ │ └── data.parquet
└── gender=female
├── country=US
│ └── data.parquet
├── country=CN
│ └── data.parquet
À la lecture, l’ensemble est reconstruit sous forme tabulaire :
root
|-- name: string (nullable = true)
|-- age: long (nullable = true)
|-- gender: string (nullable = true)
|-- country: string (nullable = true)
2.3 Un format taillé pour l’analyse - pas uniquement le big data
Comme le rappelle le blog d’Upsolver :
Complex data such as logs and event streams would need to be represented as a table with hundreds or thousands of columns, and many millions of rows. Storing this table in a row-based format such as CSV would mean:
- Queries will take longer to run since more data needs to be scanned…
- Storage will be more costly since CSVs are not compressed as efficiently as Parquet
Mais le format Parquet
n’est pas réservé aux architectures big data. Toute personne produisant ou manipulant des jeux de données bénéficiera de ses qualités :
- fichiers plus petits,
- import rapide et fiable,
2.4 Lire un Parquet
en Python
: exemple
Il existe de nombreuses librairies fonctionnant bien avec Parquet
mais les deux plus utiles à connaître sont PyArrow
et DuckDB
. Nous avons déjà évoqué succinctement celles-ci lorsqu’il était question des alternatives à Pandas
gérant mieux les données volumineuses. Ces librairies peuvent servir à effectuer les premières opérations lourdes avant de convertir les données obtenues, plus légères, en pd.DataFrame
.
La librairie PyArrow
permet de lire et écrire des fichiers Parquet
tout en tirant parti de la structure colonne du format2. Elle repose sur un objet pyarrow.Table
, qui peut, une fois les calculs lourds effectués, être converti vers un DataFrame
Pandas
pour bénéficier d’un écosystème plus riche en fonctionnalités.
La librairie DuckDB
permet d’interroger directement des fichiers Parquet
à l’aide du langage SQL
, sans les charger entièrement en mémoire. Autrement dit, elle reprend la philosophie du monde de la base de données (l’utilisation de SQL) mais sur des fichiers. Le résultat des requêtes peut, là aussi, être converti en DataFrame
Pandas
, ce qui permet de profiter à la fois de la souplesse de Pandas
et de la performance du moteur SQL embarqué. Fonctionnalité moins connue, cette librairie permet aussi d’effectuer des opérations SQL directement sur un DataFrame
Pandas
. Ceci peut être pertinent pour des situations où la syntaxe Pandas
est peu pratique là où SQL
est très bien fait ; par exemple, pour créer une nouvelle variable comme le résultat d’une statistique par groupe.
L’utilisation des alias pa
pour pyarrow
et pq
pour pyarrow.parquet
est une convention largement adoptée, à l’image de celle de pd
pour pandas
.
Pour illustrer ces fonctionnalités, prenons un jeu de données issu des données synthétiques du recensement de la population diffusés par l’Insee.
import requests
import pyarrow.parquet as pq
# Example Parquet
= "https://minio.lab.sspcloud.fr/projet-formation/bonnes-pratiques/data/RPindividus/REGION=93/part-0.parquet"
url
# Télécharger le fichier et l'enregistrer en local
with open("example.parquet", "wb") as f:
= requests.get(url)
response f.write(response.content)
L’idéal pour bénéficier pleinement des optimisations permises par le format Parquet
est de passer par pyarrow.dataset
. Cela permettra de bénéficier des optimisations permises par le combo Parquet
et Arrow
, que toutes les manières de lire un Parquet
avec Arrow
ne proposent pas (cf. prochains exercices).
import pyarrow.dataset as ds
= ds.dataset(
dataset "example.parquet"
= ["AGED", "IPONDI", "DEPT"])
).scanner(columns = dataset.to_table()
table table
pyarrow.Table
AGED: int32
IPONDI: double
DEPT: dictionary<values=string, indices=int32, ordered=0>
----
AGED: [[9,12,40,70,52,...,29,66,72,75,77],[46,76,46,32,2,...,7,5,37,29,4],...,[67,37,45,56,75,...,64,37,47,20,18],[16,25,51,6,11,...,93,90,92,21,65]]
IPONDI: [[2.73018871840726,2.73018871840726,2.73018871840726,0.954760150327854,3.75907197064638,...,3.27143319621654,4.83980378599556,4.83980378599556,4.83980378599556,4.83980378599556],[3.02627578376137,3.01215358930406,3.01215358930406,2.93136309038958,2.93136309038958,...,2.96848755763453,2.96848755763453,3.25812879950072,3.25812879950072,1.12514509319438],...,[2.57931132917563,2.85579410739065,0.845993555838931,2.50296716736141,3.70786113613679,...,3.08375347880892,2.88038807573222,3.22776230929947,3.22776230929947,3.22776230929947],[3.22776230929947,3.22776230929947,3.22776230929947,3.29380242174036,3.29380242174036,...,5.00000768518755,5.00000768518755,5.00000768518755,5.00000768518755,1.00000153703751]]
DEPT: [ -- dictionary:
["01","02","03","04","05",...,"95","971","972","973","974"] -- indices:
[5,5,5,5,5,...,5,5,5,5,5], -- dictionary:
["01","02","03","04","05",...,"95","971","972","973","974"] -- indices:
[5,5,5,5,5,...,5,5,5,5,5],..., -- dictionary:
["01","02","03","04","05",...,"95","971","972","973","974"] -- indices:
[84,84,84,84,84,...,84,84,84,84,84], -- dictionary:
["01","02","03","04","05",...,"95","971","972","973","974"] -- indices:
[84,84,84,84,84,...,84,84,84,84,84]]
Pour importer et traiter ces données, on peut conserver
les données sous le format pyarrow.Table
ou transformer en pandas.DataFrame
. La deuxième
option est plus lente mais présente l’avantage
de permettre ensuite d’appliquer toutes les
manipulations offertes par l’écosystème
pandas
qui est généralement mieux connu que
celui d’Arrow
.
import duckdb
"""
duckdb.sql(FROM read_parquet('example.parquet')
SELECT AGED, IPONDI, DEPT
""")
┌───────┬───────────────────┬─────────┐
│ AGED │ IPONDI │ DEPT │
│ int32 │ double │ varchar │
├───────┼───────────────────┼─────────┤
│ 9 │ 2.73018871840726 │ 06 │
│ 12 │ 2.73018871840726 │ 06 │
│ 40 │ 2.73018871840726 │ 06 │
│ 70 │ 0.954760150327854 │ 06 │
│ 52 │ 3.75907197064638 │ 06 │
│ 82 │ 3.21622922493506 │ 06 │
│ 6 │ 3.44170061276923 │ 06 │
│ 12 │ 3.44170061276923 │ 06 │
│ 15 │ 3.44170061276923 │ 06 │
│ 43 │ 3.44170061276923 │ 06 │
│ · │ · │ · │
│ · │ · │ · │
│ · │ · │ · │
│ 68 │ 2.73018871840726 │ 06 │
│ 35 │ 3.46310256220757 │ 06 │
│ 2 │ 3.46310256220757 │ 06 │
│ 37 │ 3.46310256220757 │ 06 │
│ 84 │ 3.69787960424482 │ 06 │
│ 81 │ 4.7717265388427 │ 06 │
│ 81 │ 4.7717265388427 │ 06 │
│ 51 │ 3.60566450823737 │ 06 │
│ 25 │ 3.60566450823737 │ 06 │
│ 13 │ 3.60566450823737 │ 06 │
├───────┴───────────────────┴─────────┤
│ ? rows (>9999 rows, 20 shown) │
└─────────────────────────────────────┘
2.5 Des exercices pour en apprendre plus
Voici une série d’exercices issues du cours de mise en production de projets data science que Romain Avouac et moi proposons à la fin du cursus d’ingénieurs de l’ENSAE.
Ces exercices illustrent progressivement quelques concepts présentés ci-dessus tout en présentant les bonnes pratiques à adopter pour traiter des données volumineuses. La correction de ces exercices est disponible sur la page du cours en question.
Tout au long de cette application, nous allons voir comment utiliser le format Parquet
de la manière la plus efficiente possible. Afin de comparer les différents formats et méthodes d’utilisation, nous allons comparer le temps d’exécution et l’usage mémoire d’une requête standard. Commençons déjà, sur un premier exemple avec une donnée légère, pour comparer les formats CSV
et Parquet
.
Pour cela, nous allons avoir besoin de récupérer des données au format Parquet
. Nous proposons d’utiliser les données détaillées et anonymisées du recensement de la population française: environ 20 millions de lignes pour 80 colonnes. Le code pour récupérer celles-ci est donné ci-dessous
Code pour récupérer les données
import pyarrow.parquet as pq
import pyarrow as pa
import os
# Définir le fichier de destination
= "data/RPindividus.parquet"
filename_table_individu
# Copier le fichier depuis le stockage distant (remplacer par une méthode adaptée si nécessaire)
1"mc cp s3/projet-formation/bonnes-pratiques/data/RPindividus.parquet data/RPindividus.parquet")
os.system(
# Charger le fichier Parquet
= pq.read_table(filename_table_individu)
table = table.to_pandas()
df
# Filtrer les données pour REGION == "24"
= df.loc[df["REGION"] == "24"]
df_filtered
# Sauvegarder en CSV
"data/RPindividus_24.csv", index=False)
df_filtered.to_csv(
# Sauvegarder en Parquet
"data/RPindividus_24.parquet") pq.write_table(pa.Table.from_pandas(df_filtered),
- 1
-
Cette ligne de code utilise l’utilitaire Minio Client disponible sur le
SSPCloud
. Si vous n’êtes pas sur cette infrastructure, vous pouvez vous référer à la boite dédiée
SSPCloud
Vous devrez remplacer la ligne
"mc cp s3/projet-formation/bonnes-pratiques/data/RPindividus.parquet data/RPindividus.parquet") os.system(
qui utilise l’outil en ligne de commande mc
par un code téléchargeant cette donnée à partir de l’URL https://projet-formation.minio.lab.sspcloud.fr/bonnes-pratiques/data/RPindividus.parquet.
Il y a de nombreuses manières de faire. Vous pouvez par exemple le faire en pur Python
avec requests
. Si vous avez curl
installé, vous pouvez aussi l’utiliser. Par l’intermédiaire de Python
, cela donnera la commande os.system("curl -o data/RPindividus.parquet https://projet-formation/bonnes-pratiques/data/RPindividus.parquet")
.
Ces exercices vont utiliser des décorateurs Python
, c’est-à-dire des fonctions qui surchargent le comportement d’une autre fonction. En l’occurrence, nous allons créer une fonction exécutant une chaîne d’opérations et la surcharger avec une autre chargée de contrôler l’usage mémoire et le temps d’exécution.
- Créer un notebook
benchmark_parquet.ipynb
afin de réaliser les différentes comparaisons de performance de l’application - Créons notre décorateur, en charge de benchmarker le code
Python
:
Dérouler pour retrouver le code du décorateur permettant de mesurer la performance
::: {#02aca146 .cell execution_count=8} ``` {.python .cell-code} import time from memory_profiler import memory_usage from functools import wraps import warnings
def convert_size(size_bytes):
if size_bytes == 0:
return "0B"
size_name = ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB")
i = int(math.floor(math.log(size_bytes, 1024)))
p = math.pow(1024, i)
s = round(size_bytes / p, 2)
return "%s %s" % (s, size_name[i])
# Decorator to measure execution time and memory usage
def measure_performance(func, return_output=False):
@wraps(func)
def wrapper(return_output=False, *args, **kwargs):
warnings.filterwarnings("ignore")
start_time = time.time()
mem_usage = memory_usage((func, args, kwargs), interval=0.1)
end_time = time.time()
warnings.filterwarnings("always")
exec_time = end_time - start_time
peak_mem = max(mem_usage) # Peak memory usage
exec_time_formatted = f"\033[92m{exec_time:.4f} sec\033[0m"
peak_mem_formatted = f"\033[92m{convert_size(1024*peak_mem)}\033[0m"
print(f"{func.__name__} - Execution Time: {exec_time_formatted} | Peak Memory Usage: {peak_mem_formatted}")
if return_output is True:
return func(*args, **kwargs)
return wrapper
:::
</details>
* Reprendre ce code pour encapsuler un code de construction d'une pyramide des âges dans une fonction `process_csv_appli1`
<details>
<summary>
Dérouler pour récupérer le code pour mesurer les performances de la lecture en CSV
</summary>
::: {#7fde4e3c .cell execution_count=9}
``` {.python .cell-code}
# Apply the decorator to functions
@measure_performance
def process_csv_appli1(*args, **kwargs):
df = pd.read_csv("data/RPindividus_24.csv")
return (
df.loc[df["DEPT"] == 36]
.groupby(["AGED", "DEPT"])["IPONDI"]
.sum().reset_index()
.rename(columns={"IPONDI": "n_indiv"})
)
:::
Exécuter
process_csv_appli1()
etprocess_csv_appli1(return_output=True)
Sur le même modèle, construire une fonction
process_parquet_appli1
basée cette fois sur le fichierdata/RPindividus_24.parquet
chargé avec la fonction read_parquet dePandas
Comparer les performances (temps d’exécution et allocation mémoire) de ces deux méthodes grâce à la fonction.
Correction complète
import math
import pandas as pd
import time
from memory_profiler import memory_usage
from functools import wraps
import warnings
def convert_size(size_bytes):
if size_bytes == 0:
return "0B"
= ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB")
size_name = int(math.floor(math.log(size_bytes, 1024)))
i = math.pow(1024, i)
p = round(size_bytes / p, 2)
s return "%s %s" % (s, size_name[i])
# Decorator to measure execution time and memory usage
def measure_performance(func, return_output=False):
@wraps(func)
def wrapper(return_output=False, *args, **kwargs):
"ignore")
warnings.filterwarnings(= time.time()
start_time = memory_usage((func, args, kwargs), interval=0.1)
mem_usage = time.time()
end_time "always")
warnings.filterwarnings(
= end_time - start_time
exec_time = max(mem_usage) # Peak memory usage
peak_mem = f"\033[92m{exec_time:.4f} sec\033[0m"
exec_time_formatted = f"\033[92m{convert_size(1024*peak_mem)}\033[0m"
peak_mem_formatted
print(f"{func.__name__} - Execution Time: {exec_time_formatted} | Peak Memory Usage: {peak_mem_formatted}")
if return_output is True:
return func(*args, **kwargs)
return wrapper
# Apply the decorator to functions
@measure_performance
def process_csv(*args, **kwargs):
= pd.read_csv("data/RPindividus_24.csv")
df return (
"DEPT"] == 36]
df.loc[df["AGED", "DEPT"])["IPONDI"]
.groupby([sum().reset_index()
.={"IPONDI": "n_indiv"})
.rename(columns
)
@measure_performance
def process_parquet(*args, **kwargs):
= pd.read_parquet("data/RPindividus_24.parquet")
df return (
"DEPT"] == "36"]
df.loc[df["AGED", "DEPT"])["IPONDI"]
.groupby([sum().reset_index()
.={"IPONDI": "n_indiv"})
.rename(columns
)
process_csv() process_parquet()
❓️ Quelle semble être la limite de la fonction read_parquet
?
On gagne déjà un temps conséquent en lecture mais on ne bénéficie pas vraiment de l’optimisation permise par Parquet
car on transforme les données directement après la lecture en DataFrame
Pandas
. On n’utilise donc pas l’une des fonctionnalités principales du format Parquet
, qui explique ses excellentes performances: le predicate pushdown qui consiste à optimiser notre traitement pour faire remonter, le plus tôt possible, les filtres sur les colonnes pour ne garder que celles vraiment utilisées dans le traitement.
La partie précédente a montré un gain de temps considérable du passage de CSV
à Parquet
. Néanmoins, l’utilisation mémoire était encore très élevée alors qu’on utilise de fait qu’une infime partie du fichier.
Dans cette partie, on va voir comment utiliser la lazy evaluation et les optimisations du plan d’exécution effectuées par Arrow
pour exploiter pleinement la puissance du format Parquet
.
- Ouvrir le fichier
data/RPindividus_24.parquet
avec pyarrow.dataset. Regarder la classe de l’objet obtenu. - Tester le code ci-dessous pour lire un échantillon de données:
(
dataset.scanner()5)
.head(
.to_pandas() )
Comprenez-vous la différence avec précédemment ? Observez dans la documentation la méthode to_table
: comprenez-vous son principe ?
- Construire une fonction
summarize_parquet_arrow
(resp.summarize_parquet_duckdb
) qui importe cette fois les données avec la fonctionpyarrow.dataset
(resp. avecDuckDB
) et effectue l’agrégation voulue. - Comparer les performances (temps d’exécution et allocation mémoire) des trois méthodes (
Parquet
lu et processé avecPandas
,Arrow
etDuckDB
) grâce à notre fonction.
Correction
Code complet de l’application
import duckdb
import pyarrow.dataset as ds
@measure_performance
def summarize_parquet_duckdb(*args, **kwargs):
= duckdb.connect(":memory:")
con = """
query FROM read_parquet('data/RPindividus_24.parquet')
SELECT AGED, DEPT, SUM(IPONDI) AS n_indiv
GROUP BY AGED, DEPT
"""
return (con.sql(query).to_df())
@measure_performance
def summarize_parquet_arrow(*args, **kwargs):
= ds.dataset("data/RPindividus_24.parquet", format="parquet")
dataset = dataset.to_table()
table = (
grouped_table
table"AGED", "DEPT"])
.group_by(["IPONDI", "sum")])
.aggregate([("AGED", "DEPT", "n_indiv"])
.rename_columns([
.to_pandas()
)
return (
grouped_table
)
process_parquet()
summarize_parquet_duckdb() summarize_parquet_arrow()
Avec l’évaluation différée, on obtient donc un processus en plusieurs temps:
Arrow
ouDuckDB
reçoit des instructions, les optimise, exécute les requêtes- Seules les données en sortie de cette chaîne sont renvoyées à
Python
Ajoutez une étape de filtre sur les lignes dans nos requêtes:
- Avec
DuckDB
, vous devez modifier la requête avec unWHERE DEPT IN ('18', '28', '36')
- Avec
Arrow
, vous devez modifier l’étapeto_table
de cette manière:dataset.to_table(filter=pc.field("DEPT").isin(['18', '28', '36']))
Correction
import pyarrow.dataset as ds
import pyarrow.compute as pc
import duckdb
@measure_performance
def summarize_filter_parquet_arrow(*args, **kwargs):
= ds.dataset("data/RPindividus.parquet", format="parquet")
dataset = dataset.to_table(filter=pc.field("DEPT").isin(['18', '28', '36']))
table = (
grouped_table
table"AGED", "DEPT"])
.group_by(["IPONDI", "sum")])
.aggregate([("AGED", "DEPT", "n_indiv"])
.rename_columns([
.to_pandas()
)
return (
grouped_table
)
@measure_performance
def summarize_filter_parquet_duckdb(*args, **kwargs):
= duckdb.connect(":memory:")
con = """
query FROM read_parquet('data/RPindividus_24.parquet')
SELECT AGED, DEPT, SUM(IPONDI) AS n_indiv
WHERE DEPT IN ('11','31','34')
GROUP BY AGED, DEPT
"""
return (con.sql(query).to_df())
summarize_filter_parquet_arrow() summarize_filter_parquet_duckdb()
❓️ Pourquoi ne gagne-t-on pas de temps avec nos filtres sur les lignes (voire pourquoi en perdons nous?) comme c’est le cas avec les filtres sur les colonnes ?
La donnée n’est pas organisée par blocs de lignes comme elle l’est par bloc de colonne. Heureusement, il existe pour cela un moyen: le partitionnement !
La lazy evaluation et les optimisations d’Arrow
apportent des gain de performance considérables. Mais on peut encore faire mieux ! Lorsqu’on sait qu’on va être amené à filter régulièrement les données selon une variable d’intérêt, on a tout intérêt à partitionner le fichier Parquet
selon cette variable.
Parcourir la documentation de la fonction
pyarrow.parquet.write_to_dataset
pour comprendre comment spécifier une clé de partitionnement lors de l’écriture d’un fichierParquet
. Plusieurs méthodes sont possibles.Importer la table complète des individus du recensement depuis
"data/RPindividus.parquet"
avec la fonctionpyarrow.dataset.dataset
et l’exporter en une table partitionnée"data/RPindividus_partitionne.parquet"
, partitionnée par la région (REGION
) et le département (DEPT
).Observer l’arborescence des fichiers de la table exportée pour voir comment la partition a été appliquée.
Modifier nos fonctions d’import, filtre et agrégations via
Arrow
ouDuckDB
pour utiliser, cette fois, leParquet
partitionné. Comparer à l’utilisation du fichier non partitionné.
Correction de la question 2 (écriture du Parquet partitionné)
import pyarrow.parquet as pq
= ds.dataset(
dataset "data/RPindividus.parquet", format="parquet"
).to_table()
pq.write_to_dataset(
dataset,="data/RPindividus_partitionne",
root_path=["REGION", "DEPT"]
partition_cols )
Correction de la question 4 (lecture du Parquet partitionné)
import pyarrow.dataset as ds
import pyarrow.compute as pc
import duckdb
@measure_performance
def summarize_filter_parquet_partitioned_arrow(*args, **kwargs):
= ds.dataset("data/RPindividus_partitionne/", partitioning="hive")
dataset = dataset.to_table(filter=pc.field("DEPT").isin(['18', '28', '36']))
table
= (
grouped_table
table"AGED", "DEPT"])
.group_by(["IPONDI", "sum")])
.aggregate([("AGED", "DEPT", "n_indiv"])
.rename_columns([
.to_pandas()
)
return (
grouped_table
)
@measure_performance
def summarize_filter_parquet_complete_arrow(*args, **kwargs):
= ds.dataset("data/RPindividus.parquet")
dataset = dataset.to_table(filter=pc.field("DEPT").isin(['18', '28', '36']))
table
= (
grouped_table
table"AGED", "DEPT"])
.group_by(["IPONDI", "sum")])
.aggregate([("AGED", "DEPT", "n_indiv"])
.rename_columns([
.to_pandas()
)
return (
grouped_table
)
@measure_performance
def summarize_filter_parquet_complete_duckdb(*args, **kwargs):
= duckdb.connect(":memory:")
con = """
query FROM read_parquet('data/RPindividus.parquet')
SELECT AGED, DEPT, SUM(IPONDI) AS n_indiv
WHERE DEPT IN ('11','31','34')
GROUP BY AGED, DEPT
"""
return (con.sql(query).to_df())
@measure_performance
def summarize_filter_parquet_partitioned_duckdb(*args, **kwargs):
= duckdb.connect(":memory:")
con = """
query FROM read_parquet('data/RPindividus_partitionne/**/*.parquet', hive_partitioning = True)
SELECT AGED, DEPT, SUM(IPONDI) AS n_indiv
WHERE DEPT IN ('11','31','34')
GROUP BY AGED, DEPT
"""
return (con.sql(query).to_df())
summarize_filter_parquet_complete_arrow()
summarize_filter_parquet_partitioned_arrow()
summarize_filter_parquet_complete_duckdb() summarize_filter_parquet_partitioned_duckdb()
❓️ Dans le cadre d’une mise à disposition de données en Parquet
, comment bien choisir la/les clé(s) de partitionnement ? Quelle est la limite à garder en tête ?
2.6 Pour aller plus loin
- La formation aux bonnes pratiques
R
etGit
développée par l’Insee avec des éléments très similaires à ceux présentés dans ce chapitre. - Un atelier sur le format
Parquet
et l’écosystèmeDuckDB
pour l’EHESS avec des exemplesR
etPython
utilisant la même source de données que l’application. - Le guide de prise en main des données du recensement au format
Parquet
avec des exemples d’utilisation deDuckDB
en WASM (directement depuis le navigateur, sans installationR
ouPython
)
3 Les données dans le cloud
Le stockage cloud, dans le contexte de la science des données, reprend le principe de services comme Dropbox
ou Google Drive
: un utilisateur accède à des fichiers distants comme s’ils étaient sur son propre disque3. Autrement dit, pour un utilisateur de Python
, la manipulation de fichiers stockés dans le cloud peut sembler identique à celle de fichiers locaux.
Mais contrairement à un dossier de type Mes Documents/monsuperfichier
, les fichiers ne résident pas sur l’ordinateur local. Ils sont hébergés sur un serveur distant, et chaque opération (lecture, écriture) passe par une connexion réseau.
3.1 Pourquoi ne pas utiliser Dropbox
ou Drive
?
Néanmoins, Dropbox
ou Drive
ne sont pas faits pour du stockage de données. Pour ces dernières, il est plus pertinent d’utiliser une technologie adaptée (voir le cours de mise en production). Les principaux fournisseurs de service cloud (AWS, GCP, Azure…) reposent sur le même principe avec un stockage orienté objet reposant sur une technologie de type S3
.
C’est pourquoi les principaux fournisseurs cloud (AWS, Google Cloud, Azure…) proposent des solutions spécifiques au stockage de données, souvent basées sur des systèmes orientés objet, dont le plus connu est S3
.
3.2 Le système S3
Le système S3
(Simple Storage Service), développé par Amazon, est devenu un standard dans le monde du stockage cloud. Il s’agit d’un système :
- fiable (réplication des données) ;
- sécurisé (données chiffrées, contrôle d’accès granulaire) ;
- scalable (adapté à des volumes massifs).
3.2.1 Le concept de bucket
L’unité centrale de S3 est le bucket : un espace de stockage (privé ou public) qui peut contenir une arborescence de fichiers.
Pour accéder à un fichier dans un bucket :
- L’utilisateur doit être autorisé (via des identifiants ou des jetons d’accès, souvent appelés tokens) ;
- Une fois authentifié, il peut lire, écrire ou modifier les fichiers à l’intérieur du bucket, à la manière d’un système de fichiers distant.
3.4 Cas pratique : stocker les données de son projet sur le SSP Cloud
Une composante essentielle de l’évaluation des projets Python
est la reproductibilité, i.e. la possibilité de retrouver les mêmes résultats à partir des mêmes données d’entrée et du même code. Dans la mesure du possible, il faut donc que votre rendu final parte des données brutes utilisées comme source dans votre projet. Si les fichiers de données source sont accessibles via une URL publique par exemple, il est idéal de les importer directement à partir de cette URL au début de votre projet (voir le TP Pandas pour un exemple d’un tel import via Pandas
).
En pratique, cela n’est pas toujours possible. Peut-être que vos données ne sont pas directement publiquement accessibles, ou bien sont disponibles sous des formats complexes qui demandent des pré-traitements avant d’être exploitables dans un format de donnée standard. Peut-être que vos données résultent d’une phase de récupération automatisée via une API ou du webscraping, auquel cas l’étape de récupération peut prendre du temps à reproduire. Par ailleurs, les sites internet évoluent fréquemment dans le temps, il est donc préférable de “figer” les données une fois l’étape de récupération effectuée. De la même façon, même s’il ne s’agit pas de données source, vous pouvez vouloir entraîner des modèles et stocker leur version entraînée, car cette étape peut également être chronophage.
Dans toutes ces situations, il est nécessaire de pouvoir stocker des données (ou des modèles). Votre dépôt Git
n’est pas le lieu adapté pour le stockage de fichiers volumineux. Un projet Python
bien construit est modulaire: il sépare le stockage du code (Git
), d’éléments de configuration (par exemple des jetons d’API qui ne doivent pas être dans le code) et du stockage des données. Cette séparation conceptuelle entre code et données permet de meilleurs projets.
Là où Git
est fait pour stocker du code, on utilise des solutions adaptées pour le stockage de fichiers. De nombreuses solutions existent pour ce faire. Sur le SSP Cloud, on propose MinIO
, une implémentation open-source du stockage S3
présenté plus haut. Ce court tutoriel vise à présenter une utilisation standard dans le cadre de vos projets.
Quelle que soit la solution de stockage retenue pour vos données/modèles, le code ayant servir à produire ces objets doit impérativement figurer dans votre dépôt de projet.
3.4.1 Partager des fichiers sur le SSP Cloud
Comme expliqué plus haut, on stocke les fichiers sur S3
dans un bucket. Sur le SSP Cloud, un bucket est créé automatiquement lors de votre création de compte, avec le même nom que votre compte SSP Cloud. L’interface Mes Fichiers vous permet d’y accéder de manière visuelle, d’y importer des fichiers, de les télécharger, etc.
Dans ce tutoriel, nous allons plutôt y accéder de manière programmatique, via du code Python
. Le package s3fs
permet de requêter votre bucket à la manière d’un filesystem classique. Par exemple, vous pouvez lister les fichiers disponibles sur votre bucket avec la commande suivante :
import s3fs
= s3fs.S3FileSystem(client_kwargs={"endpoint_url": "https://minio.lab.sspcloud.fr"})
fs
= "mon_nom_utilisateur_sspcloud"
MY_BUCKET fs.ls(MY_BUCKET)
Si vous n’avez jamais ajouté de fichier sur MinIO, votre bucket est vide, cette commande devrait donc renvoyer une liste vide. On va donc ajouter un premier dossier pour voir la différence.
Par défaut, un bucket vous est personnel, c’est à dire que les données qui s’y trouvent ne peuvent être lues ou modifiées que par vous. Dans le cadre de votre projet, vous aurez envie de partager ces fichiers avec les membres de votre groupe pour développer de manière collaborative. Mais pas seulement ! Il faudra également que vos correcteurs puissent accéder à ces fichiers pour reproduire vos analyses.
Il existe différentes possibilités de rendre des fichiers plus ou moins publics sur MinIO
. La plus simple, et celle que nous vous recommandons, est de créer un dossier diffusion
à la racine de votre bucket. Sur le SSP Cloud, tous les fichiers qui se situent dans un dossier diffusion
sont accessibles en lecture à l’ensemble des utilisateurs authentifiés. Utilisez l’interface Mes Fichiers pour créer un dossier diffusion
à la racine de votre bucket. Si tout a bien fonctionné, la commande Python
ci-dessus devrait désormais afficher le chemin mon_nom_utilisateur_sspcloud/diffusion
.
Plutôt que chaque membre du projet travaille avec ses propres fichiers sur son ordinateur, ce qui implique une synchronisation fréquente entre membres du groupe et limite la reproductibilité du fait des risques d’erreur, les fichiers sont mis sur un dépôt central, que chaque membre du groupe peut ensuite requêter.
Pour cela, il faut simplement s’accorder au sein du groupe pour utiliser le bucket d’un des membres du projet, et s’assurer que les autres membres du groupe peuvent accéder aux données, en les mettant dans le dossier diffusion
du bucket choisi.
3.4.2 Récupération et stockage de données
Maintenant que nous savons où mettre nos données sur MinIO
, regardons comment le faire en pratique depuis Python
.
Cas d’un Dataframe
Reprenons un exemple issu du cours sur les API pour simuler une étape de récupération de données coûteuse en temps.
import requests
import pandas as pd
= "https://koumoul.com/data-fair/api/v1/datasets/dpe-france/lines?format=json&q_mode=simple&qs=code_insee_commune_actualise%3A%2201450%22&size=100&select=%2A&sampling=neighbors"
url_api = requests.get(url_api).json()
response_json = pd.json_normalize(response_json["results"])
df_dpe
2) df_dpe.head(
classe_consommation_energie | tr001_modele_dpe_type_libelle | annee_construction | _geopoint | latitude | surface_thermique_lot | numero_dpe | _i | tr002_type_batiment_description | geo_adresse | ... | geo_score | classe_estimation_ges | nom_methode_dpe | tv016_departement_code | consommation_energie | date_etablissement_dpe | longitude | _score | _id | version_methode_dpe | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | E | Vente | 1 | 45.927488,5.230195 | 45.927488 | 106.87 | 1301V2000001S | 2 | Maison Individuelle | Rue du Chateau 01800 Villieu-Loyes-Mollon | ... | 0.58 | B | Méthode Facture | 01 | 286.0 | 2013-04-15 | 5.230195 | None | HJt4TdUa1W0wZiNoQkskk | NaN |
1 | G | Vente | 1960 | 45.931376,5.230461 | 45.931376 | 70.78 | 1301V1000010R | 9 | Maison Individuelle | 552 Rue Royale 01800 Villieu-Loyes-Mollon | ... | 0.34 | D | Méthode 3CL | 01 | 507.0 | 2013-04-22 | 5.230461 | None | UhMxzza1hsUo0syBh9DxH | 3CL-DPE, version 1.3 |
2 rows × 23 columns
Cette requête nous permet de récupérer un DataFrame Pandas
, dont les deux premières lignes sont imprimées ci-dessus. Dans notre cas le processus est volontairement simpliste, mais on peut imaginer que de nombreuses étapes de requêtage / préparation de la données sont nécessaires pour aboutir à un dataframe exploitable dans la suite du projet, et que ce processus est coûteux en temps. On va donc stocker ces données “intermédiaires” sur MinIO
afin de pouvoir exécuter la suite du projet sans devoir refaire tourner tout le code qui les a produites.
On peut utiliser les fonctions d’export de Pandas
, qui permettent d’exporter dans différents formats de données. Vu qu’on est dans le cloud, une étape supplémentaire est nécessaire : on ouvre une connexion vers MinIO
, puis on exporte notre dataframe.
= "mon_nom_utilisateur_sspcloud"
MY_BUCKET = f"{MY_BUCKET}/diffusion/df_dpe.csv"
FILE_PATH_OUT_S3
with fs.open(FILE_PATH_OUT_S3, 'w') as file_out:
df_dpe.to_csv(file_out)
On peut vérifier que notre fichier a bien été uploadé via l’interface Mes Fichiers ou bien directement en Python
en interrogeant le contenu du dossier diffusion
de notre bucket :
f"{MY_BUCKET}/diffusion") fs.ls(
On pourrait tout aussi simplement exporter notre dataset en Parquet
, pour limiter l’espace de stockage et maximiser les performances à la lecture. Attention : vu que Parquet
est un format compressé, il faut préciser qu’on écrit un fichier binaire : le mode d’ouverture du fichier passé à la fonction fs.open
passe de w
(write
) à wb
(write binary
).
= f"{MY_BUCKET}/diffusion/df_dpe.parquet"
FILE_PATH_OUT_S3
with fs.open(FILE_PATH_OUT_S3, 'wb') as file_out:
df_dpe.to_parquet(file_out)
Cas de fichiers
Dans la partie précédente, on était dans le cas “simple” d’un dataframe, ce qui nous permettait d’utiliser directement les fonctions d’export de Pandas
. Maintenant, imaginons qu’on ait plusieurs fichiers d’entrée, pouvant chacun avoir des formats différents. Un cas typique de tels fichiers sont les fichiers ShapeFile
, qui sont des fichiers de données géographiques, et se présentent sous forme d’une combinaison de fichiers (cf. chapitre sur GeoPandas). Commençons par récupérer un fichier .shp
pour voir sa structure.
On récupère ci-dessous les contours des départements français, sous la forme d’une archive .zip
qu’on va décompresser en local dans un dossier departements_fr
.
import io
import os
import requests
import zipfile
# Import et décompression
= "https://www.data.gouv.fr/fr/datasets/r/eb36371a-761d-44a8-93ec-3d728bec17ce"
contours_url = requests.get(contours_url, stream=True)
response = zipfile.ZipFile(io.BytesIO(response.content))
zipfile "departements_fr")
zipfile.extractall(
# Vérification du dossier (local, pas sur S3)
"departements_fr") os.listdir(
['departements-20180101.cpg',
'departements-20180101.prj',
'LICENCE.txt',
'departements-descriptif.txt',
'departements-20180101.shx',
'departements-20180101.shp',
'departements-20180101.dbf']
Vu qu’il s’agit cette fois de fichiers locaux et non d’un dataframe Pandas
, on doit utiliser le package s3fs
pour transférer les fichiers du filesystem local au filesystem distant (MinIO
). Grâce à la commande put
, on peut copier en une seule commande le dossier sur MinIO
. Attention à bien spécifier le paramètre recursive=True
, qui permet de copier à la fois un dossier et son contenu.
"departements_fr/", f"{MY_BUCKET}/diffusion/departements_fr/", recursive=True) fs.put(
Vérifions que le dossier a bien été copié :
f"{MY_BUCKET}/diffusion/departements_fr") fs.ls(
Si tout a bien fonctionné, la commande ci-dessus devrait renvoyer une liste contenant les chemins sur MinIO
des différents fichiers (.shp
, .shx
, .prj
, etc.) constitutifs du ShapeFile
des départements.
3.4.3 Utilisation des données
En sens inverse, pour récupérer les fichiers depuis MinIO
dans une session Python
, les commandes sont symétriques.
Cas d’un dataframe
Attention à bien passer cette fois le paramètre r
(read
, pour lecture) et non plus w
(write
, pour écriture) à la fonction fs.open
afin de ne pas écraser le fichier !
= "mon_nom_utilisateur_sspcloud"
MY_BUCKET = f"{MY_BUCKET}/diffusion/df_dpe.csv"
FILE_PATH_S3
# Import
with fs.open(FILE_PATH_S3, 'r') as file_in:
= pd.read_csv(file_in)
df_dpe
# Vérification
2) df_dpe.head(
De même, si le fichier est en Parquet
(en n’oubliant pas de passer de r
à rb
pour tenir compte de la compression) :
= "mon_nom_utilisateur_sspcloud"
MY_BUCKET = f"{MY_BUCKET}/diffusion/df_dpe.parquet"
FILE_PATH_S3
# Import
with fs.open(FILE_PATH_S3, 'rb') as file_in:
= pd.read_parquet(file_in)
df_dpe
# Vérification
2) df_dpe.head(
Cas de fichiers
Dans le cas de fichiers, on va devoir dans un premier temps rapatrier les fichiers de MinIO
vers la machine local (en l’occurence, le service ouvert sur le SSP Cloud).
# Récupération des fichiers depuis MinIO vers la machine locale
f"{MY_BUCKET}/diffusion/departements_fr/", "departements_fr/", recursive=True) fs.get(
Puis on les importe classiquement depuis Python
avec le package approprié. Dans le cas des ShapeFile
, où les différents fichiers sont en fait des parties d’un seul et même fichier, une seule commande permet de les importer après les avoir rappatriés.
import geopandas as gpd
= gpd.read_file("departements_fr")
df_dep 2) df_dep.head(
3.5 Pour aller plus loin
Informations additionnelles
environment files have been tested on.
Latest built version: 2025-06-18
Python version used:
'3.12.3 (main, Feb 4 2025, 14:48:35) [GCC 13.3.0]'
Package | Version |
---|---|
affine | 2.4.0 |
aiobotocore | 2.22.0 |
aiohappyeyeballs | 2.6.1 |
aiohttp | 3.11.18 |
aioitertools | 0.12.0 |
aiosignal | 1.3.2 |
altair | 5.4.1 |
annotated-types | 0.7.0 |
anyio | 4.9.0 |
appdirs | 1.4.4 |
argon2-cffi | 25.1.0 |
argon2-cffi-bindings | 21.2.0 |
arrow | 1.3.0 |
asttokens | 3.0.0 |
async-lru | 2.0.5 |
attrs | 25.3.0 |
babel | 2.17.0 |
beautifulsoup4 | 4.13.4 |
black | 24.8.0 |
bleach | 6.2.0 |
blis | 1.3.0 |
boto3 | 1.37.3 |
botocore | 1.37.3 |
branca | 0.8.1 |
Brotli | 1.1.0 |
bs4 | 0.0.2 |
cartiflette | 0.0.3 |
Cartopy | 0.24.1 |
catalogue | 2.0.10 |
cattrs | 24.1.3 |
certifi | 2025.4.26 |
cffi | 1.17.1 |
charset-normalizer | 3.4.2 |
chromedriver-autoinstaller | 0.6.4 |
click | 8.2.1 |
click-plugins | 1.1.1 |
cligj | 0.7.2 |
cloudpathlib | 0.21.1 |
comm | 0.2.2 |
commonmark | 0.9.1 |
confection | 0.1.5 |
contextily | 1.6.2 |
contourpy | 1.3.2 |
cycler | 0.12.1 |
cymem | 2.0.11 |
dataclasses-json | 0.6.7 |
debugpy | 1.8.14 |
decorator | 5.2.1 |
defusedxml | 0.7.1 |
diskcache | 5.6.3 |
duckdb | 1.3.0 |
et_xmlfile | 2.0.0 |
executing | 2.2.0 |
fastexcel | 0.14.0 |
fastjsonschema | 2.21.1 |
fiona | 1.10.1 |
folium | 0.19.6 |
fontawesomefree | 6.6.0 |
fonttools | 4.58.0 |
fqdn | 1.5.1 |
frozenlist | 1.6.0 |
fsspec | 2025.5.0 |
geographiclib | 2.0 |
geopandas | 1.0.1 |
geoplot | 0.5.1 |
geopy | 2.4.1 |
graphviz | 0.20.3 |
great-tables | 0.12.0 |
greenlet | 3.2.2 |
h11 | 0.16.0 |
htmltools | 0.6.0 |
httpcore | 1.0.9 |
httpx | 0.28.1 |
httpx-sse | 0.4.0 |
idna | 3.10 |
imageio | 2.37.0 |
importlib_metadata | 8.7.0 |
importlib_resources | 6.5.2 |
inflate64 | 1.0.1 |
ipykernel | 6.29.5 |
ipython | 9.3.0 |
ipython_pygments_lexers | 1.1.1 |
ipywidgets | 8.1.7 |
isoduration | 20.11.0 |
jedi | 0.19.2 |
Jinja2 | 3.1.6 |
jmespath | 1.0.1 |
joblib | 1.5.1 |
json5 | 0.12.0 |
jsonpatch | 1.33 |
jsonpointer | 3.0.0 |
jsonschema | 4.23.0 |
jsonschema-specifications | 2025.4.1 |
jupyter | 1.1.1 |
jupyter-cache | 1.0.0 |
jupyter_client | 8.6.3 |
jupyter-console | 6.6.3 |
jupyter_core | 5.7.2 |
jupyter-events | 0.12.0 |
jupyter-lsp | 2.2.5 |
jupyter_server | 2.16.0 |
jupyter_server_terminals | 0.5.3 |
jupyterlab | 4.4.3 |
jupyterlab_pygments | 0.3.0 |
jupyterlab_server | 2.27.3 |
jupyterlab_widgets | 3.0.15 |
kaleido | 0.2.1 |
kiwisolver | 1.4.8 |
langchain | 0.3.25 |
langchain-community | 0.3.9 |
langchain-core | 0.3.61 |
langchain-text-splitters | 0.3.8 |
langcodes | 3.5.0 |
langsmith | 0.1.147 |
language_data | 1.3.0 |
lazy_loader | 0.4 |
loguru | 0.7.3 |
lxml | 5.4.0 |
mapclassify | 2.8.1 |
marisa-trie | 1.2.1 |
Markdown | 3.8 |
markdown-it-py | 3.0.0 |
MarkupSafe | 3.0.2 |
marshmallow | 3.26.1 |
matplotlib | 3.10.3 |
matplotlib-inline | 0.1.7 |
mdurl | 0.1.2 |
mercantile | 1.2.1 |
mistune | 3.1.3 |
mizani | 0.11.4 |
multidict | 6.4.4 |
multivolumefile | 0.2.3 |
murmurhash | 1.0.13 |
mypy_extensions | 1.1.0 |
narwhals | 1.40.0 |
nbclient | 0.10.0 |
nbconvert | 7.16.6 |
nbformat | 5.10.4 |
nest-asyncio | 1.6.0 |
networkx | 3.4.2 |
nltk | 3.9.1 |
notebook | 7.4.3 |
notebook_shim | 0.2.4 |
numpy | 2.2.6 |
openpyxl | 3.1.5 |
orjson | 3.10.18 |
outcome | 1.3.0.post0 |
overrides | 7.7.0 |
OWSLib | 0.33.0 |
packaging | 24.2 |
pandas | 2.2.3 |
pandocfilters | 1.5.1 |
parso | 0.8.4 |
pathspec | 0.12.1 |
patsy | 1.0.1 |
Pebble | 5.1.1 |
pexpect | 4.9.0 |
pillow | 11.2.1 |
pip | 25.1.1 |
platformdirs | 4.3.8 |
plotly | 6.1.2 |
plotnine | 0.13.6 |
polars | 1.8.2 |
preshed | 3.0.9 |
prometheus_client | 0.22.1 |
prompt_toolkit | 3.0.51 |
propcache | 0.3.1 |
psutil | 7.0.0 |
ptyprocess | 0.7.0 |
pure_eval | 0.2.3 |
py7zr | 0.22.0 |
pyarrow | 17.0.0 |
pybcj | 1.0.6 |
pycparser | 2.22 |
pycryptodomex | 3.23.0 |
pydantic | 2.11.5 |
pydantic_core | 2.33.2 |
pydantic-settings | 2.9.1 |
Pygments | 2.19.1 |
pynsee | 0.1.8 |
pyogrio | 0.11.0 |
pyparsing | 3.2.3 |
pyppmd | 1.1.1 |
pyproj | 3.7.1 |
pyshp | 2.3.1 |
PySocks | 1.7.1 |
python-dateutil | 2.9.0.post0 |
python-dotenv | 1.0.1 |
python-json-logger | 3.3.0 |
python-magic | 0.4.27 |
pytz | 2025.2 |
pywaffle | 1.1.1 |
PyYAML | 6.0.2 |
pyzmq | 26.4.0 |
pyzstd | 0.17.0 |
rasterio | 1.4.3 |
referencing | 0.36.2 |
regex | 2024.11.6 |
requests | 2.32.3 |
requests-cache | 1.2.1 |
requests-toolbelt | 1.0.0 |
retrying | 1.3.4 |
rfc3339-validator | 0.1.4 |
rfc3986-validator | 0.1.1 |
rich | 14.0.0 |
rpds-py | 0.25.1 |
rtree | 1.4.0 |
s3fs | 2025.5.0 |
s3transfer | 0.11.3 |
scikit-image | 0.24.0 |
scikit-learn | 1.6.1 |
scipy | 1.13.0 |
seaborn | 0.13.2 |
selenium | 4.33.0 |
Send2Trash | 1.8.3 |
setuptools | 80.8.0 |
shapely | 2.1.1 |
shellingham | 1.5.4 |
six | 1.17.0 |
smart-open | 7.1.0 |
sniffio | 1.3.1 |
sortedcontainers | 2.4.0 |
soupsieve | 2.7 |
spacy | 3.8.4 |
spacy-legacy | 3.0.12 |
spacy-loggers | 1.0.5 |
SQLAlchemy | 2.0.41 |
srsly | 2.5.1 |
stack-data | 0.6.3 |
statsmodels | 0.14.4 |
tabulate | 0.9.0 |
tenacity | 9.1.2 |
terminado | 0.18.1 |
texttable | 1.7.0 |
thinc | 8.3.6 |
threadpoolctl | 3.6.0 |
tifffile | 2025.5.24 |
tinycss2 | 1.4.0 |
topojson | 1.9 |
tornado | 6.5.1 |
tqdm | 4.67.1 |
traitlets | 5.14.3 |
trio | 0.30.0 |
trio-websocket | 0.12.2 |
typer | 0.15.3 |
types-python-dateutil | 2.9.0.20250516 |
typing_extensions | 4.13.2 |
typing-inspect | 0.9.0 |
typing-inspection | 0.4.1 |
tzdata | 2025.2 |
Unidecode | 1.4.0 |
uri-template | 1.3.0 |
url-normalize | 2.2.1 |
urllib3 | 2.4.0 |
wasabi | 1.1.3 |
wcwidth | 0.2.13 |
weasel | 0.4.1 |
webcolors | 24.11.1 |
webdriver-manager | 4.0.2 |
webencodings | 0.5.1 |
websocket-client | 1.8.0 |
widgetsnbextension | 4.0.14 |
wordcloud | 1.9.3 |
wrapt | 1.17.2 |
wsproto | 1.2.0 |
xlrd | 2.0.1 |
xyzservices | 2025.4.0 |
yarl | 1.20.0 |
yellowbrick | 1.5 |
zipp | 3.21.0 |
View file history
SHA | Date | Author | Description |
---|---|---|---|
7611f138 | 2025-06-18 13:43:09 | Lino Galiana | Traduction du chapitre S3 (#616) |
e4a1e0bd | 2025-06-17 16:57:25 | lgaliana | commence la traduction du chapitre S3 |
78e4efe5 | 2025-06-17 12:40:57 | lgaliana | change variable that make pipeline fail |
14790b01 | 2025-06-16 16:57:32 | lgaliana | fix error api komoul |
8f8e6563 | 2025-06-16 15:42:32 | lgaliana | Les chapitres dans le bon ordre, ça serait mieux… |
d8bacc63 | 2025-06-16 17:34:16 | Lino Galiana | Ménage de printemps: retire les vieux chapitres inutiles (#615) |
Notes de bas de page
Pour en savoir plus sur les enjeux liés à un choix de format de données, voir @dondon2023quels.↩︎
Il est recommandé de régulièrement consulter la documentation officielle de
pyarrow
concernant la lecture et écriture de fichiers et celle relative aux manipulations de données.↩︎Ce comportement est souvent rendu possible via des systèmes de fichiers virtuels ou des wrappers compatibles avec
pandas
,pyarrow
,duckdb
, etc.↩︎
Citation
@book{galiana2023,
author = {Galiana, Lino},
title = {Python pour la data science},
date = {2023},
url = {https://pythonds.linogaliana.fr/},
doi = {10.5281/zenodo.8229676},
langid = {fr}
}
3.3 Comment faire avec Python ?
3.3.1 Les librairies principales
L’interaction entre ce système distant de fichiers et une session locale de Python est possible grâce à des API. Les deux principales librairies sont les suivantes :
Les librairies
pyarrow
etduckdb
que nous avons déjà présentées permettent également de traiter des données stockées sur le cloud comme si elles étaient sur le serveur local. C’est extrêmement pratique et permet de fiabiliser la lecture ou l’écriture de fichiers dans une architecture cloud.Sur le SSP Cloud, les jetons d’accès au stockage S3 sont injectés automatiquement dans les services lors de leur création. Ils sont ensuite valides pour une durée de 7 jours. Si l’icône du service passe du vert au rouge, cela signifie que ces jetons sont périmés, il faut donc sauvegarder son code / ses données et reprendre depuis un nouveau service.