DEV Community

Cover image for JSON dans les projets data science : Trucs & Astuces
Hakim
Hakim

Posted on

JSON dans les projets data science : Trucs & Astuces

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]}]
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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 ni dump, il faut utiliser les méthodes loads et dumps à la place.
  • Il faut utiliser des flags pour utiliser certaines fonctionnalités tels que orjson.OPT_SERIALIZE_NUMPY pour sérialiser les objets numpy
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))
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

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"},
]
Enter fullscreen mode Exit fullscreen mode

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"}
Enter fullscreen mode Exit fullscreen mode

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'}
Enter fullscreen mode Exit fullscreen mode

TL;DR

Petit résumé des astuces vues ici :

  • Utilier ensure_ascii=False lorsqu'on travaille avec la librairie standard json
  • 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

Top comments (0)