Notebook originel: ct251114_fourplot.ipynb

CT-251104 Science des données🔗

Date: 14 novembre 2025
Durée: 2 h

Ce sujet a été créé avec l’aide de ChatGPT (GPT-5, OpenAI, novembre 2025).

Consignes🔗

Créez un répertoire CT251114_Nom_Prenom/ (Utilisez ce modèle, pas autre chose!) dans lequel vous travaillerez, et où seront stockés

  1. le notebook jupyter d’analyse ct251114_nom_prenom.ipynb, que vous complèterez progressivement en ajoutant une ou plusieurs cellules en dessous de chaque question;

  2. le package python en développement, selon les instructions ci-dessous.

Vous devez alors mettre ce répertoire sous contrôle git sur un projet dédié sur le serveur https://gitlab.in2p3.fr, et en transmettre l’adresse à y.copin@ipnl.in2p3.fr. Toutefois, si, par manque de temps ou de connaissance, vous ne pouvez pas créer un dépôt git, envoyez par mail le notebook et/ou le package (sous forme d’un fichier tar ou zip); cette solution sera pénalisée dans la notation.

Tests et configuration🔗

Ce notebook inclut des tests unitaires vous permettant de tester a minima le code du notebook. Utilisez ces tests pour guider votre développement et vérifier vos résultats. Ne modifiez pas (a fortiori, n’effacez pas) ces tests.

Les deux cellules suivantes configurent le notebook, vous ne devriez pas avoir besoin d’ajouter d”import supplémentaires.

[1]:
# Ne pas utiliser d'autres librairies (p.ex. pandas!)
import numpy as np
import matplotlib.pyplot as plt
from scipy.special import erfinv
from scipy.stats import linregress
[2]:
np.set_printoptions(precision=3, suppress=True)
plt.rcParams['figure.constrained_layout.use'] = True
rng = np.random.default_rng(1234)  # Initialisation du générateur aléatoire
z = rng.standard_normal(100)

4-plot🔗

Le 4-plot est un ensemble de quatre techniques graphiques d’analyse exploratoire des données (ou des résidus à un modèle statistique) utilisées pour vérifier les hypothèses fondamentales d’un processus de mesure:

  • stabilité dans le temps (absence de tendance),

  • indépendance des observations,

  • variation constante (homoscédasticité),

  • distribution normale des erreurs (ou des valeurs mesurées).

Le 4-plot comporte donc les quatre graphiques suivants :

  • un « sequence plot » (affichage des observations vs indice \(i\))

  • un « lag plot » (observation \(i\) vs observation \(i-1\))

  • un histogramme des observations

  • un graphique de probabilité normale (normal probability plot)

Si les quatre hypothèses de base d’un processus de mesure sont respectées, ces graphiques présentent un aspect caractéristique. En revanche, toute anomalie dans l’un ou plusieurs de ces graphiques indique qu’une hypothèse ne tient pas.

Voici un exemple pour un échantillon de 100 valeurs suivant une distribution normale:

ct251114_fourplot.png

Cet exemple sert de référence pour l’implémentation du 4-plot.

Lecture et première analyse des données🔗

  1. Écrire une fonction lisant un fichier texte contenant une seule colonne numérique, et retournant un tableau numpy 1D de réels. La fonction doit lever une AssertionError si le fichier ne contient pas des données 1D.

[3]:
def load_data(filepath):

    return None
  1. Écrire une fonction qui retourne un dictionnaire contenant les clés/statistiques suivantes pour un vecteur z de données numériques :

    • mean: moyenne,

    • std: écart-type,

    • q1: premier quartile,

    • median: médiane,

    • q3: troisième quartile,

    • min: minimum,

    • max: maximum.

[4]:
def basic_stats(z):

    return dict(mean=0,
                std=0,
                q1=0,
                median=0,
                q3=0,
                min=0,
                max=0)

Sequence plot, lag plot et histogramme🔗

Le 4-plot contient quatre sous-graphiques :

  • Sequence plot: valeurs \(z_i\) en fonction de l’indice \(i\)

  • Lag plot: valeur \(z_{i}\) en fonction de \(z_{i-1}\)

  • Histogramme des valeurs \(z_i\)

  • Normal probability plot (objet de la section suivante)

  1. Implémentez les trois premiers graphiques sous forme de fonctions individuelles :

    def sequence_plot(z, ax=None):
        """Trace $z_i$ en fonction de $i$ (utilise scatter)."""
    
    def lag_plot(z, ax=None):
        """Trace $z_{i}$ en fonction de $z_{i-1}$ (utilise scatter)."""
    
    def histogram_plot(z, ax=None):
        """Trace l'histogramme des valeurs de z."""
    

    Chaque fonction doit :

    • créer un sous-graphe si ax n’est pas fourni,

    • ajouter un titre et des étiquettes d’axes, conformément à l’exemple donné ci-dessus,

    • retourner l’objet ax.

Normal probability plot🔗

L’objectif de cette partie est d’évaluer la normalité de la distribution des observations à partir d’un graphique dit de probabilité normale (Normal Probability Plot).

Ce graphique compare les quantiles observés (en ordonnées) à ceux qu’on attendrait d’une loi normale (en abscisse). S’il s’agit effectivement d’une distribution normale, les points doivent s’aligner approximativement sur une droite.

La procédure est la suivante:

  • trier les \(n\) observations \(z_i\),

  • calculer la position des probabilité empiriques: \(p_i = \frac{i - 0.5}{n}\) avec \(i = 1 \ldots n\),

  • calculer les quantiles théoriques de la loi normale: \(q_i = \Phi^{-1}(p_i)\)\(\Phi\) est la fonction de répartition (Cumulative Distribution Function) de la loi normale standard, soit \(q_i = \sqrt{2}\,\text{erf}^{-1}⁡(2 p_i - 1)\) (utiliser scipy.special.erfinv)

  • tracer le graphique (scatter) reliant quantiles théoriques \(q_i\) en abscisse et valeurs triées \(z^S_i\) en ordonnées. Ajouter la droite de régression linéaire \(z^S_i = \sigma\,q_i + \mu\), conformément à l’exemple ci-dessus.

  1. Écrire une fonction qui retourne les valeurs \(q_i\) et \(z^S_i\) nécessaire au Normal Probability Plot pour un vecteur \(z\) de données numériques.

[5]:
def normal_probability_data(z):
    """Retourne les quantités nécessaires au tracé du Normal Probability Plot."""

    return q, z_sorted
  1. Écrire une fonction traçant le Normal Probability Plot pour un vecteur \(z\) de données numériques, ainsi que la droite de régression linéaire, conformément à l’exemple donné précédemment.

[6]:
def normal_probability_plot(z, ax=None):
    """Trace le Normal Probability Plot des valeurs de z (utilise scatter)."""

    return ax
  1. Écrire une fonction regroupant, pour un vecteur \(z\) de données numériques, les 4 graphiques précédents dans une unique figure, et retournant cette figure.

[7]:
def four_plot(z):
    """
    Produit une figure 2×2 contenant :
    - Run Sequence Plot
    - Lag Plot
    - Histogramme
    - Normal Probability Plot
    et retourne la figure.
    """

    return fig
  1. Tester les fonctions précédentes pour créer le 4-plot du jeu de données ct251114_fourplot.dat.

Packaging🔗

  1. Extraire toutes les fonctions précédentes et les inclure dans un fichier fourplot.py. Il constituera le module de votre package.

  2. Documenter a minima le module et les fonctions par des docstrings (vous pouvez vous aider de l’énoncé).

  3. Créer un package fourplot_nom_prenom constitué du seul module fourplot.py, avec l’arborescence suivante:

    CT251114_Nom_Prenom/
    ├── fourplot_nom_prenom
    │   ├── fourplot.py
    │   └── __init__.py
    ├── LICENSE
    ├── README
    ├── pyproject.toml
    └── ...
    
  4. En utilisant notamment les fonctions définies à la partie précédente, ajoutez un fichier __main__.py à votre package pour pouvoir exécuter:

    $ python -m fourplot ct251114_fourplot.dat
    mean: -177.435
    std: 276.637968787728
    q1: -451.0
    median: -162.0
    q3: 93.0
    min: -579.0
    max: 300.0
    

    en affichant le 4-plot correspondant.

  5. Ajoutez un point d’entrée fourplot à votre package pour pouvoir exécuter:

    $ fourplot ct251114_fourplot.dat
    mean: -177.435
    std: 276.637968787728
    q1: -451.0
    median: -162.0
    q3: 93.0
    min: -579.0
    max: 300.0
    

    en affichant le 4-plot correspondant.

  6. Ajouter un répertoire doc/ pour configurer et générer la documentation Sphinx du package (voir documentation pyyc). Inclure votre notebook (sous doc/notebooks/) dans une section dédiée.

  7. Ajouter un répertoire test/ pour inclure les tests unitaires de ce notebook (voir documentation pyyc).

  8. Commit/push toutes vos modifications sur votre répo central git, et envoyer l’adresse de ce répo à y.copin@ipnl.in2p3.fr.

    Si vous n’avez pas réussi à stocker votre package sous git, générer une distribution des sources de votre package avec la commande

    $ python -m build
    

    (nécessite l’installation de la librairie externe build) et envoyer l’archive correspondante (générée sous dist/) à y.copin@ipnl.in2p3.fr (ajouter le notebook).

    Si vous n’avez pas non plus réussi à configurer votre package, générer manuellement une archive de votre package (incluant le notebook):

    $ tar cvzf CT251114_Nom_Prenom.tgz CT251114_Nom_Prenom/
    

    et envoyer le tarball à y.copin@ipnl.in2p3.fr.

Dans tous les cas, vous devez envoyer un mail à y.copin@ipnl.in2p3.fr.

Tests (ne pas modifier)🔗

[9]:
%%capture --no-stdout --no-stderr

import unittest

class TestFourPlotFunctions(unittest.TestCase):
    """Tests unitaires pour les fonctions du 4-plot."""

    def test_00_load_data(self):
        np.savetxt("test1.dat", np.array([1, 2, 3, 4]))
        np.savetxt("test2.dat", np.array([[1, 2], [3, 4]]))
        d = load_data("test1.dat")
        self.assertIsInstance(d, np.ndarray)
        self.assertEqual(d.shape, (4,))
        with self.assertRaises(AssertionError):
            load_data("test2.dat")

    def test_01_basic_stats(self):
        stats = basic_stats(np.arange(10))
        self.assertAlmostEqual(stats['mean'], 4.5, places=6)
        self.assertAlmostEqual(stats['std'], 2.8722813232690143, places=6)
        self.assertAlmostEqual(stats['q1'], 2.25, places=6)
        self.assertAlmostEqual(stats['median'], 4.5, places=6)
        self.assertAlmostEqual(stats['q3'], 6.75, places=6)
        self.assertAlmostEqual(stats['min'], 0, places=6)
        self.assertAlmostEqual(stats['max'], 9, places=6)

    def test_02_sequence_plot(self):
        z = np.arange(10)**2
        ax = sequence_plot(z)
        self.assertIsInstance(ax, plt.Axes)
        x, y = ax.collections[0].get_offsets().data.T
        np.testing.assert_almost_equal(x, np.arange(10))
        np.testing.assert_almost_equal(y, z)
        self.assertEqual(ax.get_title(), 'Sequence Plot')
        self.assertEqual(ax.get_xlabel(), '$i$')
        self.assertEqual(ax.get_ylabel(), '$z_{i}$')

    def test_03_lag_plot(self):
        ax = lag_plot(np.arange(10))
        self.assertIsInstance(ax, plt.Axes)
        x, y = ax.collections[0].get_offsets().data.T
        np.testing.assert_almost_equal(x, np.arange(9))
        np.testing.assert_almost_equal(y, np.arange(1, 10))
        self.assertEqual(ax.get_title(), 'Lag Plot')
        self.assertEqual(ax.get_xlabel(), '$z_{i-1}$')
        self.assertEqual(ax.get_ylabel(), '$z_{i}$')

    def test_04_histogram_plot(self):
        ax = histogram_plot(np.arange(10)**2)
        self.assertIsInstance(ax, plt.Axes)
        n = [ int(rect.get_height()) for rect in ax.containers[0] ]
        np.testing.assert_array_equal(n, [3, 2, 0, 1, 1, 0, 1, 1, 0, 1])
        self.assertEqual(ax.get_title(), 'Histogram')
        self.assertEqual(ax.get_xlabel(), 'Value')
        self.assertEqual(ax.get_ylabel(), 'Frequency')

    def test_05_normal_data_x(self):
        x, y = normal_probability_data(np.concat((np.arange(5), np.arange(-5, 0))))
        np.testing.assert_almost_equal(
            x,
            [-1.645, -1.036, -0.674, -0.385, -0.126, 0.126, 0.385, 0.674, 1.036, 1.645],
            decimal=3)

    def test_05_normal_data_y(self):
        x, y = normal_probability_data(np.concat((np.arange(5), np.arange(-5, 0))))
        np.testing.assert_almost_equal(y, np.arange(-5, 5))

    def test_06_normal_plot(self):
        ax = normal_probability_plot(np.concat((np.arange(5), np.arange(-5, 0))))
        self.assertIsInstance(ax, plt.Axes)
        x, y = ax.collections[0].get_offsets().data.T
        np.testing.assert_almost_equal(
            x,
            [-1.645, -1.036, -0.674, -0.385, -0.126, 0.126, 0.385, 0.674, 1.036, 1.645],
            decimal=3)
        np.testing.assert_almost_equal(y, np.arange(-5, 5))
        self.assertEqual(ax.get_title(), 'Normal Probability Plot')
        self.assertEqual(ax.get_xlabel(), 'Theoretical Quantiles')
        self.assertEqual(ax.get_ylabel(), 'Ordered Values')

    def test_07_normal_plot_line(self):
        ax = normal_probability_plot(np.concat((np.arange(5), np.arange(-5, 0))))
        x, y = ax.get_lines()[0].get_data()
        slope, = np.diff(y[[0, -1]]) / np.diff(x[[0, -1]])
        inter = y[0] - slope * x[0]
        self.assertAlmostEqual(slope, 3.0362778308730456, places=6)
        self.assertAlmostEqual(inter, -0.5, places=6)

    def test_08_four_plot(self):
        fig = four_plot(np.arange(10))
        self.assertIsInstance(fig, plt.Figure)
        self.assertEqual(len(fig.axes), 4)

# Pour exécuter les tests directement dans le notebook
unittest.main(argv=[''], verbosity=2, exit=False)
test_00_load_data (__main__.TestFourPlotFunctions.test_00_load_data) ... FAIL
test_01_basic_stats (__main__.TestFourPlotFunctions.test_01_basic_stats) ... FAIL
test_02_sequence_plot (__main__.TestFourPlotFunctions.test_02_sequence_plot) ... ERROR
test_03_lag_plot (__main__.TestFourPlotFunctions.test_03_lag_plot) ... ERROR
test_04_histogram_plot (__main__.TestFourPlotFunctions.test_04_histogram_plot) ... ERROR
test_05_normal_data_x (__main__.TestFourPlotFunctions.test_05_normal_data_x) ... ERROR
test_05_normal_data_y (__main__.TestFourPlotFunctions.test_05_normal_data_y) ... ERROR
test_06_normal_plot (__main__.TestFourPlotFunctions.test_06_normal_plot) ... FAIL
test_07_normal_plot_line (__main__.TestFourPlotFunctions.test_07_normal_plot_line) ... ERROR
test_08_four_plot (__main__.TestFourPlotFunctions.test_08_four_plot) ... ERROR

======================================================================
ERROR: test_02_sequence_plot (__main__.TestFourPlotFunctions.test_02_sequence_plot)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/tmp/ipykernel_1429730/1812238449.py", line 27, in test_02_sequence_plot
    ax = sequence_plot(z)
         ^^^^^^^^^^^^^
NameError: name 'sequence_plot' is not defined

======================================================================
ERROR: test_03_lag_plot (__main__.TestFourPlotFunctions.test_03_lag_plot)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/tmp/ipykernel_1429730/1812238449.py", line 37, in test_03_lag_plot
    ax = lag_plot(np.arange(10))
         ^^^^^^^^
NameError: name 'lag_plot' is not defined

======================================================================
ERROR: test_04_histogram_plot (__main__.TestFourPlotFunctions.test_04_histogram_plot)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/tmp/ipykernel_1429730/1812238449.py", line 47, in test_04_histogram_plot
    ax = histogram_plot(np.arange(10)**2)
         ^^^^^^^^^^^^^^
NameError: name 'histogram_plot' is not defined

======================================================================
ERROR: test_05_normal_data_x (__main__.TestFourPlotFunctions.test_05_normal_data_x)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/tmp/ipykernel_1429730/1812238449.py", line 56, in test_05_normal_data_x
    x, y = normal_probability_data(np.concat((np.arange(5), np.arange(-5, 0))))
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/tmp/ipykernel_1429730/3331342372.py", line 4, in normal_probability_data
    return q, z_sorted
           ^
NameError: name 'q' is not defined

======================================================================
ERROR: test_05_normal_data_y (__main__.TestFourPlotFunctions.test_05_normal_data_y)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/tmp/ipykernel_1429730/1812238449.py", line 63, in test_05_normal_data_y
    x, y = normal_probability_data(np.concat((np.arange(5), np.arange(-5, 0))))
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/tmp/ipykernel_1429730/3331342372.py", line 4, in normal_probability_data
    return q, z_sorted
           ^
NameError: name 'q' is not defined

======================================================================
ERROR: test_07_normal_plot_line (__main__.TestFourPlotFunctions.test_07_normal_plot_line)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/tmp/ipykernel_1429730/1812238449.py", line 81, in test_07_normal_plot_line
    x, y = ax.get_lines()[0].get_data()
           ^^^^^^^^^^^^
AttributeError: 'NoneType' object has no attribute 'get_lines'

======================================================================
ERROR: test_08_four_plot (__main__.TestFourPlotFunctions.test_08_four_plot)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/tmp/ipykernel_1429730/1812238449.py", line 88, in test_08_four_plot
    fig = four_plot(np.arange(10))
          ^^^^^^^^^^^^^^^^^^^^^^^^
  File "/tmp/ipykernel_1429730/1310614070.py", line 11, in four_plot
    return fig
           ^^^
NameError: name 'fig' is not defined

======================================================================
FAIL: test_00_load_data (__main__.TestFourPlotFunctions.test_00_load_data)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/tmp/ipykernel_1429730/1812238449.py", line 10, in test_00_load_data
    self.assertIsInstance(d, np.ndarray)
AssertionError: None is not an instance of <class 'numpy.ndarray'>

======================================================================
FAIL: test_01_basic_stats (__main__.TestFourPlotFunctions.test_01_basic_stats)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/tmp/ipykernel_1429730/1812238449.py", line 17, in test_01_basic_stats
    self.assertAlmostEqual(stats['mean'], 4.5, places=6)
AssertionError: 0 != 4.5 within 6 places (4.5 difference)

======================================================================
FAIL: test_06_normal_plot (__main__.TestFourPlotFunctions.test_06_normal_plot)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/tmp/ipykernel_1429730/1812238449.py", line 68, in test_06_normal_plot
    self.assertIsInstance(ax, plt.Axes)
AssertionError: None is not an instance of <class 'matplotlib.axes._axes.Axes'>

----------------------------------------------------------------------
Ran 10 tests in 0.034s

FAILED (failures=3, errors=7)
[ ]:

Cette page a été générée à partir de ct251114_fourplot.ipynb.