Notebook originel: ct251114_fourplot_sol.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):
    """
    Lit un fichier texte ou CSV contenant une seule colonne numérique.
    Retourne un tableau numpy 1D de float.
    """

    arr = np.loadtxt(filepath)

    assert arr.ndim == 1

    return arr
  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):
    """
    Retourne un dictionnaire contenant les statistiques
    'mean', 'std', 'q1', 'median', 'q3', 'min', 'max' du vecteur z.
    """

    zarr = np.asarray(z)
    q1, med, q3 = np.percentile(zarr, [25, 50, 75])

    return dict(mean=zarr.mean(),
                std=zarr.std(),
                q1=q1,
                median=med,
                q3=q3,
                min=zarr.min(),
                max=zarr.max())

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.

[5]:
def sequence_plot(z, ax=None):
    """Trace $z_i$ en fonction de $i$ (scatter)."""

    if ax is None:
        fig, ax = plt.subplots()

    ax.scatter(np.arange(len(z)), z, marker='+', color='k')
    ax.set(title='Sequence Plot',
           xlabel='$i$',
           ylabel='$z_{i}$')

    return ax
[6]:
def lag_plot(z, ax=None):
    """Trace $z_{i}$ en fonction de $z_{i-1}$ (scatter)."""

    if ax is None:
        fig, ax = plt.subplots()

    ax.scatter(z[:-1], z[1:], marker='+', color='k')
    ax.set(title='Lag Plot',
           xlabel='$z_{i-1}$',
           ylabel='$z_{i}$')

    return ax
[7]:
def histogram_plot(z, ax=None):
    """Trace l'histogramme des valeurs de z."""

    if ax is None:
        fig, ax = plt.subplots()

    ax.hist(z, color='k')
    ax.set(title='Histogram',
           xlabel='Value',
           ylabel='Frequency')

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

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

    z_sorted = np.sort(z)
    n = len(z_sorted)
    p = (np.arange(1, n+1) - 0.5) / n
    q = np.sqrt(2) * erfinv(2*p - 1)

    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.

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

    if ax is None:
        fig, ax = plt.subplots()

    q, z_sorted = normal_probability_data(z)

    ax.scatter(q, z_sorted, marker='+', color='k')
    result = linregress(q, z_sorted)
    ax.plot(q[[0, -1]], result.slope * q[[0, -1]] + result.intercept,
            ls='--', c='r', label=f"µ={result.intercept:.3f}, σ={result.slope:.3f})")
    ax.set(title='Normal Probability Plot',
           xlabel='Theoretical Quantiles',
           ylabel='Ordered Values')
    ax.legend()

    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.

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

    fig, ((axs, axl), (axh, axp)) = plt.subplots(2, 2, layout='constrained')

    sequence_plot(z, ax=axs)
    lag_plot(z, ax=axl)
    histogram_plot(z, ax=axh)
    normal_probability_plot(z, ax=axp)

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

[11]:
z = load_data("ct251114_fourplot.dat")
stats = basic_stats(z)
for key, val in stats.items():
    print(f"{key}: {val}")
four_plot(z);
mean: -177.435
std: 276.637968787728
q1: -451.0
median: -162.0
q3: 93.0
min: -579.0
max: 300.0
../../_images/Annales_25-fourplot_ct251114_fourplot_sol_22_2.png

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

[12]:
%%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) ... ok
test_01_basic_stats (__main__.TestFourPlotFunctions.test_01_basic_stats) ... ok
test_02_sequence_plot (__main__.TestFourPlotFunctions.test_02_sequence_plot) ... ok
test_03_lag_plot (__main__.TestFourPlotFunctions.test_03_lag_plot) ... ok
test_04_histogram_plot (__main__.TestFourPlotFunctions.test_04_histogram_plot) ... ok
test_05_normal_data_x (__main__.TestFourPlotFunctions.test_05_normal_data_x) ... ok
test_05_normal_data_y (__main__.TestFourPlotFunctions.test_05_normal_data_y) ... ok
test_06_normal_plot (__main__.TestFourPlotFunctions.test_06_normal_plot) ... ok
test_07_normal_plot_line (__main__.TestFourPlotFunctions.test_07_normal_plot_line) ... ok
test_08_four_plot (__main__.TestFourPlotFunctions.test_08_four_plot) ... ok

----------------------------------------------------------------------
Ran 10 tests in 0.189s

OK
[ ]:

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