Notebook originel: exam24_corrige.ipynb
Examen Science des données: estimateurs robustes de la dispersion🔗
Date: 5 novembre 2024, 10h00-12h00
Durée: 2 h
L’objectif général de l’exercice est de mettre en place des estimateurs robustes de la dispersion d’un échantillon, c.à.d des estimateurs moins sensibles à la présence éventuelle de points abérrants (i.e. des points ne respectant pas la distribution intrinsèque de la population) dans l’échantillon.
Consignes🔗
Créez un répertoire Exam24_NOM_Prenom/ dans lequel vous travaillerez, et où seront stockés
le notebook jupyter d’analyse (
nom_prenom.ipynb, copié à partir de ce notebook énoncé sans l’extension.orig), 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) à y.copin@ipnl.in2p3.fr (cette solution sera pénalisée dans la notation).
Tests🔗
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.
Si vous n’avez pas réussi à stocker votre package sous ``git``, envoyer votre notebook à y.copin@ipnl.in2p3.fr.
[1]:
%matplotlib inline
[2]:
import numpy as np
import matplotlib.pyplot as plt
import scipy.stats
[3]:
np.set_printoptions(precision=3, suppress=True);
CORRECTION🔗
Référence: https://www.statsmodels.org/dev/_modules/statsmodels/robust/scale.html
Détermination robuste de la dispersion d’un échantillon🔗
Considérons un échantillon monovarié \(x = \{x_i\}_{i=1\ldots N}\). Les statistiques suivantes sont des estimateurs de la dispersion de l’échantillon, renormalisés pour fournir l’écart type dans le cas d’une distribution normale \(\mathcal{N}\).
Écart type🔗
Median Absolute Deviation🔗
où \(\Phi\) est la the fonction de distribution cumulative normale standard (scipy.stats.norm.cdf), et son inverse \(\Phi^{-1}\) est la fonction quantile normale (scipy.stats.norm.ppf).
Écart Inter-Quartile🔗
Il s’agit de la plage tronquée à 25%, c’est-à-dire la différence entre le 75e et le 25e percentile d’un échantillon.
Les estimateurs MAD et IQR ont tous deux un point de rupture optimal de 0,5 : jusqu’à la moitié des points de l’échantillon peuvent être arbitrairement grands avant que l’estimateur donne une estimation arbitrairement grande. (En comparaison, l’écart-type de l’échantillon a un point de rupture de 0.) Malheureusement, ces estimateurs sont également moins efficaces statistiquement que l’écart type traditionnel.
Par ailleurs, ces estimateurs sont légèrement biaisés (voir Croux & Rousseeuw, 1992), notamment pour les petits échantillons, mais nous n’aborderons pas ce point dans cet examen.
Différences absolues par pairs🔗
D’autres estimateurs de dispersion avec un point de rupture maximal sont présentés dans Croux and Rousseeuw (1992):
Nous les implémenterons dans une version simplifiée.
Échantillons de test (avec et sans outliers)🔗
[4]:
m, n = 10, 8
rng = np.random.default_rng(0)
# Échantillon sans outliers
arr_no = rng.normal(size=m * n)
# Échantillon avec outliers
arr_wo = arr_no.copy()
for i in range(0, m*n, n+1):
arr_wo[i] += (-1)**i * 6
Questions🔗
Tracer un histogramme normalisé de ces deux échantillons (
ax.hist(density=True)), ainsi que la distribution normale \(\mathcal{N}(\mu=0, \sigma=1)\) (scipy.stats.norm.pdf).
[5]:
fig, ax = plt.subplots()
bins = np.linspace(-10, +10, 21)
ax.hist(arr_no, bins=bins, density=True, histtype='step', label="no outliers")
ax.hist(arr_wo, bins=bins, density=True, histtype='step', label="w/ outliers")
xs = np.linspace(-3, +3)
ax.plot(xs, scipy.stats.norm.pdf(xs), label="N(0, 1)")
ax.set(xlabel="Value", ylabel="PDF")
ax.legend()
fig.savefig("histo.png")
Écrire une fonction
sigma_std_1D(arr)qui retourne l’écart type de l’échantillon (np.std(ddof=1)).Pour cette fonction et les suivantes, s’assurer (
assert) que le tableau d’entrée est unidimensionnel.
[6]:
def sigma_std_1D(arr):
assert np.ndim(arr) == 1
return np.std(arr, ddof=1)
[7]:
sigma_std_1D(arr_no), sigma_std_1D(arr_wo),
[7]:
(0.9697001436773827, 2.326360764433049)
Écrire les fonctions
mad_1D(arr)etsigma_mad_1D(arr)qui retourne le MAD normalisé de l’échantillon (utilisernp.medianetscipy.stats.norm.ppf).
[8]:
def mad_1D(arr):
assert np.ndim(arr) == 1
return np.median(np.abs(arr - np.median(arr)))
[9]:
mad_1D(arr_no), mad_1D(arr_wo)
[9]:
(0.7009412573844651, 0.8302102070751392)
[10]:
def sigma_mad_1D(arr, small_sample=False):
assert np.ndim(arr) == 1
norm = scipy.stats.norm.ppf(0.75)
# print(f"{norm=}, {1/norm=}")
if small_sample:
# Williams (2011) via Akinshin (2022, 2207.12005)
bns = [None, None, 1.197, 1.490, 1.360, 1.217, 1.189, 1.138, 1.127, 1.101]
n, = np.shape(arr)
if n <= 9:
bn = bns[n]
else:
bn = n / (n - 0.801)
norm *= bn
return mad_1D(arr) / norm
[11]:
sigma_mad_1D(arr_no), sigma_mad_1D(arr_wo)
[11]:
(1.0392170632403142, 1.2308714948355965)
Écrire les fonctions
iqr_1D(arr)etsigma_iqr_1D(arr)qui retourne l’écart inter-quartile normalisé de l’échantillon (utilisernp.quantile).
[12]:
def iqr_1D(arr):
assert np.ndim(arr) == 1
q1, q2 = np.quantile(arr, (.25, .75))
return q2 - q1
[13]:
iqr_1D(arr_no), iqr_1D(arr_wo)
[13]:
(1.4012263172056918, 1.5901879638726397)
[14]:
def sigma_iqr_1D(arr):
assert np.ndim(arr) == 1
norm = 2 * scipy.stats.norm.ppf(0.75)
# print(f"{norm=}, {1/norm=}")
return iqr_1D(arr) / norm
[15]:
sigma_iqr_1D(arr_no), sigma_iqr_1D(arr_wo)
[15]:
(1.0387306232587965, 1.1788081015392409)
Pour l’estimateur \(Sn\), il est d’abord nécessaire de déterminer la normalisation \(c\), qui résout cette équation:
\[\Phi\left(\Phi^{-1}(3/4) + 1/c\right) - \Phi\left(\Phi^{-1}(3/4) - 1/c\right) - 1/2 = 0\]Définir la fonction
objfn(c)dont on cherche le zéro.Tracer cette fonction sur l’intervale \([1/2, 2]\).
Écrire une fonction
Sn_norm()qui retourne la solution de l’équation précédente.
[16]:
def objfn(c):
return scipy.stats.norm.cdf(scipy.stats.norm.ppf(0.75) + 1/c) - scipy.stats.norm.cdf(scipy.stats.norm.ppf(0.75) - 1/c) - 0.5
[17]:
objfn(1)
[17]:
0.0805853432046475
[18]:
xs = np.geomspace(0.5, 2)
fig, ax = plt.subplots()
ax.plot(xs, objfn(xs))
plt.show();
[19]:
def Sn_norm():
from scipy.optimize import brentq
zero = brentq(objfn, 0.5, 2)
return zero
[20]:
Sn_norm()
[20]:
1.1925985531232088
Implémentation (naïve et approximative) de l’estimateur \(Sn\)
Écrire une fonction
all_pairs_absdiff(arr)qui retourne le tableau \(N\times N\) des différences absolues \(\left[\,|x_i - x_j|\,\right]_{i,j = 1\ldots N}\) (utiliser une double liste en compréhension).Écrire la fonction
sigma_Sn_1D(arr)qui retourne l’estimateur \(Sn\) normalisé de l’échantillon (utiliser \(c = 1.19259855\) si vous n’avez pas sû répondre à la question 5.C).
[21]:
def all_pairs_absdiff(arr):
return np.abs([ [ xi - xj for xi in arr ] for xj in arr ])
[22]:
all_pairs_absdiff(np.arange(3))
[22]:
array([[0, 1, 2],
[1, 0, 1],
[2, 1, 0]])
[23]:
def sigma_Sn_1D(arr, c=1.19259855):
assert np.ndim(arr) == 1
return c * np.median(np.median(all_pairs_absdiff(arr), axis=0))
[24]:
sigma_Sn_1D(arr_no), sigma_Sn_1D(arr_wo)
[24]:
(0.9833810779051267, 1.1717986891987089)
Implémentation (naïve et approximative) de l’estimateur \(Qn\).
En utilisant la fonction
upper_pairs_absdiff(arr)ci-dessous, écrire la fonctionsigma_Qn_1D(arr)qui retourne l’estimateur \(Qn\) normalisé de l’échantillon (utilisernp.quantile).
[25]:
def upper_pairs_absdiff(arr):
n, = np.shape(arr)
idx = np.triu_indices(n, k=1)
return np.abs(arr[idx[0]] - arr[idx[1]])
[26]:
upper_pairs_absdiff(np.arange(3))
[26]:
array([1, 2, 1])
[27]:
def sigma_Qn_1D(arr):
assert np.ndim(arr) == 1
qn = np.quantile(upper_pairs_absdiff(arr), 0.25)
norm = 2**0.5 * scipy.stats.norm.ppf(5/8)
#print(f"{norm=}, {1/norm=}")
return qn / norm
[28]:
sigma_Qn_1D(arr_no), sigma_Qn_1D(arr_wo)
[28]:
(1.059319495196456, 1.2943757905148814)
Écrire les fonctions
sigma_mad(arr, axis=None)etsigma_iqr(arr, axis=None)généralisant les fonctions unidimensionnelles précédentes aux tableaux de rang arbitraire, et en calculant les statistiques le long des axes spécifiés (Nonepour l’ensemble du tableau).
[29]:
def sigma_mad(arr, axis=None):
return np.median(np.abs(arr - np.median(arr, axis=axis, keepdims=True)), axis=axis) / scipy.stats.norm.ppf(0.75)
[30]:
sigma_mad(arr_no.reshape(m, n)), sigma_mad(arr_no.reshape(m, n), axis=0), sigma_mad(arr_no.reshape(m, n), axis=1)
[30]:
(1.0392170632403142,
array([1.073, 0.859, 1.46 , 1.263, 0.395, 0.719, 1.715, 1.262]),
array([0.573, 0.761, 0.803, 0.727, 0.637, 0.926, 0.978, 1.054, 0.866,
1.678]))
[31]:
def sigma_iqr(arr, axis=None):
q1, q2 = np.quantile(arr, (0.25, 0.75), axis=axis)
return (q2 - q1) / (2 * scipy.stats.norm.ppf(0.75))
[32]:
sigma_iqr(arr_no.reshape(m, n)), sigma_iqr(arr_no.reshape(m, n), axis=0), sigma_iqr(arr_no.reshape(m, n), axis=1)
[32]:
(1.0387306232587965,
array([0.914, 0.711, 1.174, 1.135, 0.436, 0.647, 1.555, 1.057]),
array([0.498, 0.54 , 0.699, 0.677, 0.548, 1.002, 0.835, 0.953, 0.644,
1.362]))
Packaging🔗
Extraire toutes les fonctions précédentes et les inclure dans un fichier
scale.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
Exam24_Nom_Prenomconstitué du seul modulescale.py, avec l’arborescence suivante:Exam24_Nom_Prenom/ ├── scale_nom_prenom │ ├── scale.py │ └── __init__.py ├── LICENSE ├── README ├── setup.cfg └── setup.py
Ajouter un répertoire
doc/pour configurer et générer la documentation Sphinx du package (voir documentation pyyc). Y 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 setup.py sdist
et envoyer l’archive correspondante (générée sous
dist/) à y.copin@ipnl.in2p3.fr. Si vous n’avez pas non plus réussi à configurer votre package, générer manuellement une archive de votre package:$ tar cvzf Exam23_NOM_Prenom.tgz Exam24_NOM_Prenom/
et envoyer le tarball.
Tests (ne pas modifier)🔗
[35]:
import unittest
class TestNotebook(unittest.TestCase):
def setUp(self):
m, n = self.m, self.n = 10, 8
rng = np.random.default_rng(0)
self.arr_no = rng.normal(size=m * n) # Échantillon sans outliers
self.arr_wo = self.arr_no.copy() # Échantillon avec outliers
for i in range(0, m*n, n+1):
self.arr_wo[i] += (-1)**i * 6
self.arr2D = self.arr_no.reshape((m, n))
self.places = 6
def test_01a_std_shape(self):
with self.assertRaises(AssertionError):
sigma_std_1D(self.arr2D) # Invalid shape
def test_01b_std(self):
self.assertAlmostEqual(sigma_std_1D(self.arr_no), 0.9697001436773827, places=self.places)
self.assertAlmostEqual(sigma_std_1D(self.arr_wo), 2.326360764433049, places=self.places)
def test_02a_mad_shape(self):
with self.assertRaises(AssertionError):
mad_1D(self.arr2D) # Invalid shape
def test_02b_mad(self):
self.assertAlmostEqual(mad_1D(self.arr_no), 0.7009412573844651, places=self.places)
self.assertAlmostEqual(mad_1D(self.arr_wo), 0.8302102070751392, places=self.places)
def test_02c_sigma_mad(self):
self.assertAlmostEqual(sigma_mad_1D(self.arr_no), 1.0392170632403142, places=self.places)
self.assertAlmostEqual(sigma_mad_1D(self.arr_wo), 1.2308714948355965, places=self.places)
def test_03a_iqr_shape(self):
with self.assertRaises(AssertionError):
iqr_1D(self.arr2D) # Invalid shape
def test_03b_iqr(self):
self.assertAlmostEqual(iqr_1D(self.arr_no), 1.4012263172056918, places=self.places)
self.assertAlmostEqual(iqr_1D(self.arr_wo), 1.5901879638726397, places=self.places)
def test_03c_sigma_iqr(self):
self.assertAlmostEqual(sigma_iqr_1D(self.arr_no), 1.0387306232587965, places=self.places)
self.assertAlmostEqual(sigma_iqr_1D(self.arr_wo), 1.1788081015392409, places=self.places)
def test_04a_objfn(self):
self.assertAlmostEqual(objfn(1), 0.0805853432046475, places=self.places)
def test_04b_Sn_norm(self):
self.assertAlmostEqual(Sn_norm(), 1.1925985531232088, places=12)
def test_05a_all_pairs(self):
np.testing.assert_array_equal(all_pairs_absdiff(np.arange(3)), [[0, 1, 2], [1, 0, 1], [2, 1, 0]])
def test_05b_Sn_shape(self):
with self.assertRaises(AssertionError):
sigma_Sn_1D(self.arr2D) # Invalid shape
def test_05c_Sn(self):
self.assertAlmostEqual(sigma_Sn_1D(self.arr_no), 0.9833810779051267, places=self.places)
self.assertAlmostEqual(sigma_Sn_1D(self.arr_wo), 1.1717986891987089, places=self.places)
def test_06a_upper_pairs(self):
np.testing.assert_array_equal(upper_pairs_absdiff(np.arange(3)), [1, 2, 1])
def test_06b_Qn_shape(self):
with self.assertRaises(AssertionError):
sigma_Qn_1D(self.arr2D) # Invalid shape
def test_06c_Qn(self):
self.assertAlmostEqual(sigma_Qn_1D(self.arr_no), 1.059319495196456, places=self.places)
self.assertAlmostEqual(sigma_Qn_1D(self.arr_wo), 1.2943757905148814, places=self.places)
def test_07a_sigma_mad_nd(self):
self.assertAlmostEqual(sigma_mad(self.arr2D), 1.0392170632403142, places=self.places)
np.testing.assert_array_almost_equal(sigma_mad(self.arr2D, axis=0),
[1.073203, 0.858985, 1.459733, 1.263337,
0.395109, 0.719357, 1.715088, 1.262144])
np.testing.assert_array_almost_equal(sigma_mad(self.arr2D, axis=1),
[0.572676, 0.761405, 0.803225, 0.727297, 0.636682,
0.926232, 0.97819 , 1.053814, 0.866482, 1.677964])
def test_07b_sigma_iqr_nd(self):
self.assertAlmostEqual(sigma_iqr(self.arr2D), 1.0387306232587965, places=self.places)
np.testing.assert_array_almost_equal(sigma_iqr(self.arr2D, axis=0),
[0.914075, 0.710991, 1.173704, 1.135337,
0.436489, 0.647078, 1.554884, 1.056983])
np.testing.assert_array_almost_equal(sigma_iqr(self.arr2D, axis=1),
[0.497738, 0.540138, 0.698781, 0.677263, 0.547558,
1.002339, 0.835115, 0.952673, 0.643802, 1.361982])
#unittest.TestLoader.sortTestMethodsUsing = lambda *args: +1
unittest.main(argv=[''], verbosity=2, exit=False);
test_01a_std_shape (__main__.TestNotebook) ... ok
test_01b_std (__main__.TestNotebook) ... ok
test_02a_mad_shape (__main__.TestNotebook) ... ok
test_02b_mad (__main__.TestNotebook) ... ok
test_02c_sigma_mad (__main__.TestNotebook) ... ok
test_03a_iqr_shape (__main__.TestNotebook) ... ok
test_03b_iqr (__main__.TestNotebook) ... ok
test_03c_sigma_iqr (__main__.TestNotebook) ... ok
test_04a_objfn (__main__.TestNotebook) ... ok
test_04b_Sn_norm (__main__.TestNotebook) ... ok
test_05a_all_pairs (__main__.TestNotebook) ... ok
test_05b_Sn_shape (__main__.TestNotebook) ... ok
test_05c_Sn (__main__.TestNotebook) ... ok
test_06a_upper_pairs (__main__.TestNotebook) ... ok
test_06b_Qn_shape (__main__.TestNotebook) ... ok
test_06c_Qn (__main__.TestNotebook) ... ok
test_07a_sigma_mad_nd (__main__.TestNotebook) ... ok
test_07b_sigma_iqr_nd (__main__.TestNotebook) ... ok
----------------------------------------------------------------------
Ran 18 tests in 0.022s
OK
[ ]:
Cette page a été générée à partir de
exam24_corrige.ipynb.