Quelques astuces et librairies utiles pour manipuler du json dans vos projets de data science.
Le module standard json
Python possède un module standard nommé json
qui permet de manipuler rapidement des fichiers JSON
Chargement
import json
with open("data/example.json", "r") as f:
data = json.load(f)
data
# [{'id': 0, 'content': [0.0, 0.0, 1.0]}, {'id': 1, 'content': [0.0, 1.0, 0.0]}]
Sauvegarde
Première astuce : lorsque l’on travaille avec des données textuelles l’option ensure_ascii=False
est très utile
pour préserver, entre autres, les accents lors de la sauvegarde
with open("data/example.json", "w") as f:
json.dump(data, f, ensure_ascii=False)
Deuxième astuce : l’option indent
de la méthode dump
permet d’indenter les données dans le fichier de sauvegarde
with open("data/example.json", "w") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
Les problématiques liées à numpy
On utilise très souvent numpy
dans les projets de data science.
Or les objets numpy
ne sont pas JSON-sérialisable et nécessite donc une conversion en objets standards python pour
être sauvegardés :
import numpy as np
data = np.array([[0., 0., 1.], [0., 1., 0.]])
with open("data/numpy.json", "w") as f:
json.dump(data, f, ensure_ascii=False)
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[12], line 6
3 data = np.array([[0., 0., 1.], [0., 1., 0.]])
5 with open("data/numpy.json", "w") as f:
----> 6 json.dump(data, f, ensure_ascii=False)
TypeError: Object of type ndarray is not JSON serializable
En convertissant le tableau en liste, on parvient à sauvegarder l’objet data
:
with open("data/numpy.json", "w") as f:
json.dump(data.tolist(), f, ensure_ascii=False)
Mais ce n’est pas très pratique…
Une solution est de créer un JSONEncoder
personnalisé qui convertit les numpy.ndarray
grâce à leur méthode tolist
au moment de la sauvegarde :
class NumpyJSONEncoder(json.JSONEncoder):
"""JSONEncoder to store python dict or list containing numpy arrays"""
def default(self, obj):
"""Transform numpy arrays into JSON serializable object such as list
see : https://docs.python.org/3/library/json.html#json.JSONEncoder.default
"""
if isinstance(obj, np.ndarray):
return obj.tolist()
return json.JSONEncoder.default(self, obj)
with open("data/numpy.json", "w") as f:
json.dump(data, f, ensure_ascii=False, cls=NumpyJSONEncoder)
La librairie orjson
orjson
est la librairie JSON la plus rapide disponible pour python. De
plus elle gère nativement les objets dataclass,
datetime, numpy
et UUID ce qui est très pratique.
Quelques éléments à retenir lorsque l’on travaille avec orjson
:
- Il n’y a pas de méthode
load
nidump
, il faut utiliser les méthodesloads
etdumps
à la place. - Il faut utiliser des flags pour utiliser certaines fonctionnalités tels que
orjson.OPT_SERIALIZE_NUMPY
pour sérialiser les objetsnumpy
import orjson
with open("data/example.json", "rb") as f:
data = orjson.loads(f.read())
with open("data/example.json", "wb") as f:
f.write(orjson.dumps(data, option=orjson.OPT_SERIALIZE_NUMPY))
A noter que l’on écrit dans le fichier json en binaire (d’où rb
et wb
).
Performances
orjson annonce une sérialisation des numpy.ndarray
4 à 12 fois plus rapide qu’avec la librairie standard. On peut le
vérifier sur un exemple en comparant les deux méthodes présentées plus haut :
data = {i: np.random.randn(100) for i in range(100)}
def save_json():
with open("data/fast.json", "w") as f:
json.dump(data, f, ensure_ascii=False, cls=NumpyJSONEncoder)
def save_orjson():
with open("data/orfast.json", "wb") as f:
f.write(orjson.dumps(data, option=orjson.OPT_SERIALIZE_NUMPY|orjson.OPT_NON_STR_KEYS))
%timeit save_json()
%timeit save_orjson()
# 15.5 ms ± 251 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
# 1.15 ms ± 66.3 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
Dans cet exemple orjson est plus de 10 fois plus rapide. A noter que l’option OPT_NON_STR_KEYS
est utilisé pour
permettre à orjson de sauvegarder des clés qui ne sont pas des chaines de caractères.
(Tests exécutés avec python 3.11)
FastAPI et orjson
La documentation de FastAPI
contient un guide pour utiliser orjson
pour sérialiser les réponses JSON.
C’est particulièrement pratique pour les API qui exposent des modèles de machine learning dont les sorties sont souvent
des numpy.ndarray
import numpy as np
from fastapi import FastAPI
from fastapi.responses import ORJSONResponse
app = FastAPI()
@app.get("/random-vector", response_class=ORJSONResponse)
async def get_random_vector():
return ORJSONResponse(np.random.randn(100))
JSON Lines
Il arrive très souvent que l’on manipule des collections d’objets lorsque l’on travaille avec le format JSON.
[
{"id": 0, "name": "toto"},
{"id": 1, "name": "titi"},
]
Pour être valide, le objets doivent être contenus dans une liste JSON, d’où les crochets encadrant les objets de la collection. Cependant cela n’est pas du tout pratique pour lire de gros volume de données puisqu’il faut parser l’intégraliter du fichier et tout charger en mémoire.
Pour remédier à cela, on peut utilise le format JSON Lines. Il ne s’agit ni plus ni moins que de placer un objet JSON par ligne de façon à pouvoir parcourir les objets sans devoir parser l’intégralité de la collection d’un seul coup.
{"id": 0, "name": "toto"}
{"id": 1, "name": "titi"}
La librairie jsonlines
est très pratique pour manipuler
de tels fichiers. De plus on peut la combiner avec orjson
.
import jsonlines
import orjson
with jsonlines.open("data/many_examples.jsonl", "r", loads=orjson.loads) as reader:
for obj in reader:
print(obj)
# {'id': 0, 'name': 'toto'}
# {'id': 1, 'name': 'titi'}
TL;DR
Petit résumé des astuces vues ici :
- Utilier
ensure_ascii=False
lorsqu’on travaille avec la librairie standardjson
- Considérer la librairie
orjson
pour les performances et les fonctionnalités qu’elle apporte - Penser au format
JSON Lines
pour les collections d’objets JSON