skip to content
🪓 Logo HacheNotes

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]}]

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 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))

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 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

Commentaires