Notebook originel: ct251114_fourplot_sol.ipynb
CT-251104 Science des données🔗
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
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;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:
Cet exemple sert de référence pour l’implémentation du 4-plot.
Lecture et première analyse des données🔗
É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
AssertionErrorsi 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
Écrire une fonction qui retourne un dictionnaire contenant les clés/statistiques suivantes pour un vecteur
zde 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)
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
axn’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)\) où \(\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.
É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
É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
É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
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
Packaging🔗
Extraire toutes les fonctions précédentes et les inclure dans un fichier
fourplot.py. Il constituera le module de votre package.Documenter a minima le module et les fonctions par des docstrings (vous pouvez vous aider de l’énoncé).
Créer un package
fourplot_nom_prenomconstitué du seul modulefourplot.py, avec l’arborescence suivante:CT251114_Nom_Prenom/ ├── fourplot_nom_prenom │ ├── fourplot.py │ └── __init__.py ├── LICENSE ├── README ├── pyproject.toml └── ...
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.
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.
Ajouter un répertoire
doc/pour configurer et générer la documentation Sphinx du package (voir documentation pyyc). Inclure votre notebook (sousdoc/notebooks/) dans une section dédiée.Ajouter un répertoire
test/pour inclure les tests unitaires de ce notebook (voir documentation pyyc).Commit/pushtoutes vos modifications sur votre répo centralgit, 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.