[Actualité] Python : Test d'adéquation d'une série statistique à une loi de probabilité par la méthode du khi carré
par
, 10/05/2023 à 09h16 (6615 Affichages)
I. Introduction
En statistique, le test du khi carré, aussi dit du khi-deux, d’après sa désignation symbolique χ2, est un test statistique où la statistique de test suit une loi du χ2 sous l'hypothèse nulle.
Par exemple, il permet de tester l'adéquation d'une série de données à une famille de lois de probabilité ou de tester l'indépendance entre deux variables aléatoires.
On souhaite dans notre cas implémenter le test du khi carré à l'aide de la librairie Python scipy.stats, pour nous permettre ensuite de vérifier qu'une série statistique suit une certaine loi de probabilité (Loi uniforme, loi de poisson, etc.).
Note : les exemples proposés dans ce billet ainsi que les définitions sont tous issus de la page wikipedia Test du χ2
II. Principe du test d'adéquation
Le test du χ2 d'adéquation ou de conformité permet de vérifier si un échantillon d'une variable aléatoire Y donne des observations comparables à celles d'une loi de probabilité P définie a priori dont on pense, pour des raisons théoriques ou pratiques, qu'elle devrait être la loi de Y.
L’hypothèse nulle (notée H0) est donc la suivante : « la variable aléatoire Y suit la loi de probabilité P ».
En termes de p-valeur, l'hypothèse nulle (l'observation est suffisamment proche de la théorie) est généralement rejetée lorsque p ≤ 0.05.
Il s'agit donc de vérifier qu'une série de données statistiques suit bien une loi de probabilité définie a priori, comme une loi de Poisson pour une variable discrète, ou une loi normale pour une variable continue.
II-A. Adéquation à une loi uniforme
On va maintenant tester l'hypothèse selon laquelle un dé à six faces n'est pas truqué, avec un risque α = 0.05.
L'hypothèse nulle H0 que l'on souhaite rejeter est donc ici : « Le dé est équilibré ».
Si le dé est lancé 600 fois de suite et s'il est équilibré, on s'attend donc que sur ces 600 jets, chaque chiffre tombe près de 100 fois.
Autrement dit, si le dé est équilibré, alors la variable k, représentant le numéro de la face supérieure, suit une loi uniforme. Elle peut ainsi prendre les 6 valeurs (1, 2, 3,.., 6) avec la même probabilité 1/6 pour chaque valeur.
Vérifier l'hypothèse « Le dé est équilibré », revient donc à vérifier que la variable k suit bien une loi uniforme.
Supposons que notre expérience donne les résultats suivants :
En considérant l'hypothèse nulle vraie, la valeur de la variable T du khi carré est donnée par la formule :
T = (88 − 100)2/100 + (109 − 100)2/100 + (107 − 1002/100 + (94 − 100)2/100 + (105 − 100)2/100 + (97 − 100)2/100 = 3.44
Le nombre de degrés de liberté est ici de 6-1 = 5. En effet, 88 + 109 + 107 + 94 + 105 + 97 = 600 et si l'on connaît par exemple les nombres de fois où l'on obtient les chiffres 1 à 5, on connaît le nombre de fois où l'on obtient le chiffre 6 : 600 - (88 + 109 + 107 + 94 + 105) = 97.
Ainsi, la statistique T suit la loi du χ2 à cinq degrés de liberté.
Cette loi du χ2 donne la valeur en deçà de laquelle on considère le tirage comme conforme avec un risque α = 0.05 :
P(T < 11.07) = 0.95
Puisque 3.50 < 11.07, on ne peut pas rejeter l'hypothèse nulle : ces données statistiques ne permettent pas de considérer que le dé est truqué, et on ne peut pas non plus rejeter l'hypothèse que la variable k suive une loi uniforme.
II-B. Conformité à une loi de Poisson
On considère une variable aléatoire Y prenant des valeurs entières positives ou nulles. Un échantillonnage de 100 valeurs de cette variable se répartit comme suit :
On souhaite tester l'hypothèse selon laquelle Y suit une loi de Poisson, avec un risque α = 0.05.
La valeur du paramètre de cette loi de Poisson est obtenue en calculant la moyenne pondérée de la série statistique, ce qui donne ici λ = 1,02.
Comme il s'agit d'une estimation on diminuera le nombre de degrés de liberté d'une unité.
Les effectifs attendus pour une loi de Poisson de paramètre λ sont :
On regroupe les effectifs supérieurs ou égaux à 3 dans une même classe, ceux supérieurs à 4 étant trop petits. La variable T prend alors la valeur 2.97. Or, la loi du χ2 à deux degrés de liberté donne :
P(T < 5.99) = 0.95
Donc, on ne rejette pas l'hypothèse que la variable aléatoire Y suive une loi de Poisson, au risque d'erreur de 5 %.
Si vous souhaitez avoir plus de précisions sur ce test vous pouvez consulter cette page wikipedia.
III. Module scipy.stats
Cette librairie Python contient un grand nombre de distributions de probabilités (distribution uniforme, de Poisson, etc..) ainsi que des fonctions permettant d'effectuer des tests statistiques comme le test du khi-carré.
III-A. Loi uniforme
Un objet uniform est utilisé pour une variable aléatoire continue suivant une loi uniforme :
Dans sa forme standard, la distribution est uniforme sur [0, 1]. En utilisant les paramètres loc=a et scale=b-a, on obtient une distribution uniforme sur [loc, loc + scale].
Code Python : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8 from scipy import stats # fonction de répartition de la loi uniforme continue sur l'intervalle [0, 1] p1 = stats.uniform.cdf(0.5) # Fonction de répartition cdf - calcul de P(X ≤ 0.5) = 0.5 # fonction de répartition de la loi uniforme sur l'intervalle [1, 10] p2 = stats.uniform.cdf(x=5, loc=1,scale=10-1) # Fonction de répartition cdf - calcul de P(X ≤ 5)
III-B. Loi de Poisson
Un objet poisson est utilisé pour une variable aléatoire discrète suivant une loi de Poisson :
La fonction de masse en k de cette loi est appelée ainsi :
p1 = stats.poisson.pmf(k, mu).
où k désigne la variable aléatoire discrète et le paramètre mu la moyenne λ de la série statistique.
La fonction de répartition en k :
p2 = stats.poisson.cdf(k, mu).
Code Python : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7 from scipy import stats # fonction de masse en k=2 de la loi de Poisson de paramètre 1.02 p1 = stats.poisson.pmf(2, 1.02) # fonction de répartition en k=2 de la loi de Poisson de paramètre 1.02 p2 = stats.poisson.cdf(2, 1.02)
III-C. Loi normale
Un objet norm est utilisé pour une variable aléatoire continue suivant une loi normale.
Le paramètre loc désigne la moyenne et l'argument scale l'écart type.
La fonction de répartition de la loi normale centrée réduite en x est appelée ainsi :
p1 = stats.norm.cdf(x).
La fonction de répartition de la loi normale pour une variable aléatoire de moyenne m et d'écart type s :
p2 = stats.norm.cdf(x, loc=m, scale=s).
Code Python : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7 from scipy import stats # fonction de répartition en x=0.2 de la loi normale centrée et réduite N(0,1) p1 = stats.norm.cdf(0.2) # calcul de la probabilité P(X ≤ 0.2) # fonction de répartition en x=3.5 de la loi normale N(2.5, 4) p2 = stats.norm.cdf(3.5, loc=2.5, scale=4) # calcul de la probabilité P(X ≤ 3.5)
III-D. Test du khi carré
La fonction scipy.stats.chisquare(f_obs, f_esp, ddof=degres_liberte) est utilisée pour vérifier la conformité d'une série statistique à une loi de Probabilité.
Elle a comme arguments :
- f_obs : tableau des effectifs observés
- f_exp : tableau des effectifs attendus (optionnel). Ils sont évalués à l'aide d'une fonction de densité ou de répartition suivant une probabilité.
- ddof : valeur entière représentant le nombre de degrés de liberté.
Valeurs retournées (T, pvalue) :
- T : valeur de type float résultat du test du khi carré.
- pvalue : valeur numérique comprise entre 0 et 1 indiquant si on peut ou pas accepter l'hypothèse nulle (elle est généralement rejetée lorsque p ≤ 0,05).
Code Python : Sélectionner tout - Visualiser dans une fenêtre à part
1
2 # test si les effectifs observées sont conformes aux effectifs estimées à l'aide d'une fonction de probabilité resultat_test = stats.chisquare(effectifs_obs, effectifs_est, ddof=degres_liberte)
IV. Implémentation du test du khi carré
Comme on l'a vu précédemment, le test du khi carré s'effectue sur une série de valeurs.
Nous avons donc besoin de mémoriser dans des listes les valeurs et les effectifs de la série statistique, mais aussi de définir les fonctions Python pour chacune des lois de probabilités ainsi que pour le test du chi carré.
On va donc naturellement créer une classe Python intégrant ces attributs et ces méthodes. :
Notre classe comportera un constructeur, c'est-à-dire une méthode particulière __init__() dont le code est exécuté quand la classe est instanciée.
Elle va nous permettre d'initialiser les listes des valeurs et des effectifs avec les arguments de cette méthode au moment de la création de l'objet :
Code Python : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8 class SerieStat(): def __init__(self, valeurs, effectifs): # méthode constructeur de la classe self.valeurs = valeurs # valeurs de la série statistique self.effectifs = effectifs # effectifs des valeurs self.test_actif = None # pas de test actif au moment de la création de l'objet #...
Pour tester cette méthode, nous ajoutons ces lignes au module :
Code Python : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9 # série de valeurs avec leurs effectifs valeurs = [1, 2, 3, 4, 5, 6] effectifs = [88, 109, 107, 94, 105, 97] # création d'un objet SerieStat avec la série de valeurs et les effectifs serie_stat = SerieStat(valeurs, effectifs) print("valeurs = " + str(serie_stat.valeurs)) print("effectifs = " + str(serie_stat.effectifs))
Le code affiche :
valeurs = [1, 2, 3, 4, 5, 6]
effectifs = [88, 109, 107, 94, 105, 97]
IV-A. Fonctions de probabilité
Ensuite, il nous faut ajouter à notre classe les différentes fonctions de probabilité qui doivent toutes avoir la même structure :
Code Python : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4 def fonction_proba(self, x, cumulative = False): ... return ..
Arguments des fonctions de probabilité :
- x ou k: valeur pour laquelle on souhaite calculer la probabilité ;
- cumulative: indique si la fonction est cumulative ou pas, valeur False par défaut.
Les paramètres comme la moyenne, l'écart type, la valeur mini ou la valeur maxi, seront tous obtenus à partir de la série de valeurs.
IV-A-1. Loi uniforme
Nous ajoutons maintenant des méthodes à notre classe afin de déterminer les effectifs des valeurs selon une loi uniforme discrète et selon une loi uniforme continue.
Ces fonctions vont donc nous permettre de tester l'adéquation de la série de valeurs à ces lois :
Code Python : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35 class SerieStat(): ... def uniforme_discrete(self, k, cumulative = False): # loi uniforme discrète pour k ϵ {1, 2, ..., n} : fonction de masse ou de répartition en k # nombre n de valeurs différentes n = len(self.valeurs) if not cumulative: # si c'est une fonction de masse. # retourne la probabilité P(X = k) return 1/n else: # sinon c'est une fonction de répartition # retourne la probabilité P(X ≤ k) return k/n def uniforme_continue(self, x, cumulative = False): # loi uniforme sur [a, b] : fonction cumulative ou non cumulative # intervalle entre 2 valeurs consécutives : u = (self.valeurs[-1]-self.valeurs[0])/(len(self.valeurs)-1) # intervalle total des valeurs [a, b] : [x0-u/2, xn + u/2] a = self.valeurs[0] - u/2; b = self.valeurs[-1] + u/2 if not cumulative: # si la fonction n'est pas cumulative. # valeurs de x1 et x2, avec [x1, x2] centré sur x x1 = x - u/2; x2 = x + u/2 # renvoie la probabilité P(x1 ≤ X ≤ x2) donnée par la valeur de la fonction de répartition entre x1 et x2 return stats.uniform.cdf(x2,loc=a,scale=b-a) - stats.uniform.cdf(x1,loc=a,scale=b-a) else: # sinon # renvoie la probabilité P(a ≤ X ≤ x+u/2) donnée par la valeur de la fonction de répartition entre a et x+u/2 return (stats.uniform.cdf(x+u/2,loc=a,scale=b-a) - stats.uniform.cdf(a,loc=a,scale=b-a))
La méthode uniforme_continue utilise la fonction de répartition stats.uniform.cdf(x,loc=a,scale=b-a) du module scipy.stats.
Pour tester une des méthodes, nous ajoutons simplement ces lignes de code :
Code Python : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12 # série de valeurs avec les effectifs valeurs = [1, 2, 3, 4, 5, 6] effectifs = [88, 109, 107, 94, 105, 97] # création d'un objet SerieStat avec la série de valeurs et les effectifs serie_stat = SerieStat(valeurs, effectifs) # probabilité d'avoir un 6 : P(k=3) = 1/6 = 0.16666666666666.. p = serie_stat.uniforme_discrete(3) # affiche le résultat print("P(k=3) = " + str(p))
Le code affiche :
P(k=3) = 0.16666666666666666
IV-A-2. Loi de Poisson
Nous ajoutons maintenant une autre méthode pour obtenir l'effectif de chacune des valeurs selon une loi de Poisson et ainsi pouvoir tester la conformité de la série de valeurs à cette loi :
Code Python : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 class SerieStat(): ... def poisson(self, k, cumulative=False): # loi de Poisson de paramètre m : fonction de masse ou de répartition en k # moyenne pondérée de la série statistique m = self.moyenne_ponderee() if not cumulative: # si pas cumulative # retourne la valeur en k de la fonction de masse de paramètre m return stats.poisson.pmf(k,m) else: # sinon # retourne la valeur en k de la fonction de répartition de paramètre m return stats.poisson.cdf(k,m)
Cette méthode utilise la fonction de masse stats.poisson.pmf(x,m) et de répartition stats.poisson.cdf(x,m) du module scipy.stats. La moyenne pondérée de la série statistique est évaluée à l'aide de la méthode moyenne_ponderee() présente dans cette même classe.
Pour effectuer les tests nous ajoutons simplement ces lignes de code :
Code Python : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11 # série de valeurs avec leurs effectifs valeurs = [0, 1, 2, 3, 4] effectifs = [31, 45, 16, 7, 1] # création d'un objet SerieStat avec la série de valeurs et les effectifs serie_stat = SerieStat(valeurs, effectifs) # calcul de la probabilité d'avoir le 2 : P(k=2) = 0.18758148787803533 p = serie_stat.poisson(2) # affiche le résultat print("P(k=2) = " + str(p))
Le code affiche :
P(k=2) = 0.18758148787803533
La classe contient également une méthode donnant la valeur de la fonction de répartition de la loi normale.
IV-B. Test du khi carré
Nous définissons enfin une méthode test_khi2 permettant de vérifier qu'une série de valeurs suit bien une loi de probabilité.
Cette fonction a en particulier un paramètre fonction_proba qui est un objet de type fonction correspondant à la fonction de probabilité à utiliser pour le test :
Code Python : Sélectionner tout - Visualiser dans une fenêtre à part
1
2 # test si la série de valeurs suit une loi uniforme discrète resultat_test = serie_stat.test_khi2(serie_stat.uniforme_discrete, "Loi uniforme discrète")
Arguments de la fonction :
- fonction_proba : objet de type fonction représentant la fonction de probabilité à utiliser pour le test : serie_stat.uniforme_discrete ;
- nom_loi : nom de la loi de probabilité prise en compte pour le test : "Loi uniforme discrète", "Loi de Poisson", etc. ;
- indice_debut et indice_fin : indices dans la liste de valeurs de la 1re et de la dernière classe prise en compte pour le test (arguments optionnels) ;
- degres_liberte: nombre de degrés de liberté (argument optionnel).
Elle renvoie un objet Test défini à partir d'une dataclass :
Code Python : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10 # dataclass utilisée pour définir un objet Test @dataclass class Test: type_test: str # type de test : "khi2", etc. loi_probabilite: str # loi de probabilité : "Loi de Poisson" moyenne: float # moyenne de la série statistique ecart_type: float # écart type de la série statistique degres_liberte: int # nombre de degrés de liberté pour le test T: float # valeur de T résultat du test du khi2 pvalue: float # p-value
Exemple de résultat renvoyé :
Code Python : Sélectionner tout - Visualiser dans une fenêtre à part
1
2 Test(type_test='khi2', loi_probabilite='Loi de Poisson', moyenne=1.02, ecart_type=0.9162968951164245, degres_liberte=2, T=2.9714065378260637, pvalue=0.0847481400891209)
On donne maintenant le code complet de la méthode test_khi2 à ajouter à la classe SerieStat :
Code Python : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54 class SerieStat(): ... def test_khi2(self, fonction_proba, nom_loi, indice_debut = None, indice_fin = None, degres_liberte=None): # si le nombre de degrés de liberté est omis alors on prend le nombre de valeurs différentes - 1 if degres_liberte is None: degres_liberte = len(valeurs)-1 # la série ne change pas : pas de réduction de la série de valeurs classes, effectifs_obs = (self.valeurs, self.effectifs) else: # sinon # regroupement des effectifs des valeurs mini et maxi de la série : classes = valeurs[indice_debut:indice_fin+1] classes, effectifs_obs = self.regrouper(indice_debut, indice_fin) # initialisation de la liste des effectifs attendus ou estimés effectifs_est=[] # moyenne pondérée de la série statistique m = self.moyenne_ponderee() # écart type de la série statistique s = self.ecart_type() # copie des paramètres du test actif dans l'attribut test_actif de la classe self.test_actif = Test('khi2', nom_loi, m, s, degres_liberte, None, None) # parcours des classes du milieu : classe d'indice 1 -> classe d'indice (n-1) for ci in classes[1:-1]: # calcul de l'effectif estimé pour xi : P(X = xi)*effectif_total ei = fonction_proba(ci)*self.effectif_total() effectifs_est.append(ei) # ajout de l'effetif à la liste # calcul de l'effectif estimé pour la 1re classe : P(X ≤ xi)*effectif_total e0 = fonction_proba(classes[0],cumulative=True)*self.effectif_total() # calcul de l'effectif estimé pour la dernière classe : (1 - P(X < xi))*effectif_total en = (1-fonction_proba(classes[-2],cumulative=True))*self.effectif_total() effectifs_est = [e0] + effectifs_est + [en] # résultat du test du khi carré : (T, pvalue) result_test = stats.chisquare(effectifs_obs, effectifs_est, ddof=degres_liberte) # copie de la valeur de T et de p-value dans self.test_actif self.test_actif.T = result_test[0] self.test_actif.pvalue = 1 - stats.chi2.cdf(self.test_actif.T, 1) # ou self.test_actif.pvalue = result_test[1] # copie du contenu de self.test_actif dans une variable resultat_test = self.test_actif self.test_actif = None # désactive le test # retourne le résultat du test sous la forme : Test(type_test='khi2', loi_probabilite='..', moyenne=.., ecart_type=.., degres_liberte=.., T=.., pvalue=..) return resultat_test
La méthode test_khi2 utilise la fonction stats.chisquare(f_obs, f_esp, ddof) du module scipy.stats.
Le choix des classes et le regroupement des effectifs des valeurs mini et maxi de la série est réalisée à l'aide de la méthode regrouper() présente dans cette même classe.
Pour effectuer les tests nous ajoutons enfin ces lignes de code :
Code Python : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12 # série de valeurs avec les effectifs valeurs = [0, 1, 2, 3, 4] effectifs = [31, 45, 16, 7, 1] # création d'un objet SerieStat avec la série de valeurs et les effectifs serie_stat = SerieStat(valeurs, effectifs) # test si la série de valeurs suit une loi de Poisson avec regroupement des effectifs supérieurs ou égaux à 3 dans une même classe resultat_test = serie_stat.test_khi2(serie_stat.poisson, "Loi de Poisson", indice_debut=0, indice_fin=3, degres_liberte = degres_liberte) # affiche le résultat du test du khi carré print(resultat_test)
Le code affiche :
Test(type_test='khi2', loi_probabilite='Loi de Poisson', moyenne=1.02, ecart_type=0.9162968951164245, degres_liberte=2, T=2.9714065378260637, pvalue=0.0847481400891209)
V. Module complet
Le module contenant la classe SerieStat avec en plus une méthode pour tracer sur un même graphique les points de la série statistique et ceux de la fonction de masse choisie :
Code Python : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342 from scipy import stats # librairie contenant les distributions de probabilité et la fonction de test du khi carré import matplotlib.pyplot as plt from dataclasses import dataclass # dataclass utilisée pour définir un objet Test @dataclass class Test: type_test: str # type de test : "khi2", etc. loi_probabilite: str # loi de probabilité : "Loi de Poisson" moyenne: float # moyenne de la série statistique ecart_type: float # écart type de la série statistique degres_liberte: int # nombre de degrés de liberté pour le test T: float # valeur de T résultat du test du khi2 pvalue: float # p-value class SerieStat(): def __init__(self, valeurs, effectifs): # méthode constructeur de la classe self.valeurs = valeurs # valeurs de la série statistique self.effectifs = effectifs # effectifs des valeurs self.test_actif = None # pas de test actif au moment de la création de l'objet def effectif_total(self): # renvoie l'effectif total des valeurs return sum(self.effectifs) def moyenne_ponderee(self): # calcul de la moyenne pondérée de la série de valeurs if self.test_actif: # si 1 test est actif : moyenne déjà calculée lors du test # renvoie la moyenne des valeurs du test actif return self.test_actif.moyenne else: # sinon # initialisation de la somme pondérée somme_ponderee = 0 # parcours des indices des valeurs for i in range(len(valeurs)): somme_ponderee += effectifs[i]*valeurs[i] # somme_ponderee = somme_ponderee + ni*xi # renvoie la moyenne pondérée : somme_ponderee / effectif_total return somme_ponderee/self.effectif_total() def ecart_type(self): # calcul de l'écart type de la série de valeurs if self.test_actif: # si 1 test est actif : écart type déjà calculé lors du test # renvoie l'écart type des valeurs du test actif return self.test_actif.ecart_type else: # moyenne pondérée m = self.moyenne_ponderee() # initialisation de la somme somme_ecarts = 0 # parcours des indices des valeurs for i in range(len(valeurs)): somme_ecarts += effectifs[i]*(valeurs[i] - m)**2 # somme_ecarts = somme_ecarts + ni*(xi-m)^2 # renvoie l'écart type : racine carrée de la somme obtenue divisée par l'effectif total return pow(somme_ecarts/self.effectif_total(),0.5) def uniforme_discrete(self, k, cumulative = False): # loi uniforme discrète pour k ϵ {1, 2, ..., n} : fonction de masse ou de répartition en k # nombre n de valeurs différentes n = len(self.valeurs) if not cumulative: # si c'est une fonction de masse. # retourne la probabilité P(X = k) return 1/n else: # sinon c'est une fonction de répartition # retourne la probabilité P(X ≤ k) return k/n def uniforme_continue(self, x, cumulative = False): # loi uniforme sur [a, b] : fonction cumulative ou non cumulative # intervalle entre 2 valeurs consécutives : u = (self.valeurs[-1]-self.valeurs[0])/(len(self.valeurs)-1) # intervalle total des valeurs [a, b] : [x0-u/2, xn + u/2] a = self.valeurs[0] - u/2; b = self.valeurs[-1] + u/2 if not cumulative: # si la fonction n'est pas cumulative. # valeurs de x1 et x2, avec [x1, x2] centré sur x x1 = x - u/2; x2 = x + u/2 # renvoie la probabilité P(x1 ≤ X ≤ x2) donnée par la valeur de la fonction de répartition entre x1 et x2 return stats.uniform.cdf(x2,loc=a,scale=b-a) - stats.uniform.cdf(x1,loc=a,scale=b-a) else: # sinon # renvoie la probabilité P(a ≤ X ≤ x+u/2) donnée par la valeur de la fonction de répartition entre a et x+u/2 return (stats.uniform.cdf(x+u/2,loc=a,scale=b-a) - stats.uniform.cdf(a,loc=a,scale=b-a)) def poisson(self, k, cumulative=False): # loi de Poisson de paramètre m : fonction de masse ou de répartition en k # moyenne pondérée de la série statistique m = self.moyenne_ponderee() if not cumulative: # si pas cumulative # retourne la valeur en k de la fonction de masse de paramètre m return stats.poisson.pmf(k,m) else: # sinon # retourne la valeur en k de la fonction de répartition de paramètre m return stats.poisson.cdf(k,m) def normale(self, x, cumulative=False): # loi Normale de moyenne m et d'écart type s : fonction de répartition en x # intervalle entre 2 valeurs consécutives : u = (self.valeurs[-1]-self.valeurs[0])/(len(self.valeurs)-1) # moyenne pondérée de la série statistique m = self.moyenne_ponderee() # écart type de la série statistique s = self.ecart_type() if not cumulative: # si la fonction n'est pas cumulative. # valeurs de x1 et x2, avec [x1, x2] centré sur x x1 = x - u/2; x2 = x + u/2 # retourne la valeur de la fonction de répartition de la loi normale N(m, e) entre x1 et x2 return (stats.norm.cdf(x2, loc=m, scale=s) - stats.norm.cdf(x1, loc=m, scale=s)) else: # sinon # retourne la valeur de la fonction de répartition de la loi normale N(m, e) en x+u/2 return stats.norm.cdf(x+u/2, loc=m, scale=s) def regrouper(self, indice_debut, indice_fin): # regroupement des valeurs mini et maxi de la série dans 2 classes (première et dernière classe de la série) # regroupement des effectifs des valeurs mini et maxi de la série # sélection des classes prise en compte classes = self.valeurs[indice_debut:indice_fin+1] # calculs des effectifs pour la première et la dernière classe e0 = sum(self.effectifs[0:indice_debut+1]) en = sum(self.effectifs[indice_fin:]) # ajout des effectifs en tête et en queue de liste effectifs = [e0] + self.effectifs[indice_debut+1:indice_fin] + [en] return (classes, effectifs) def test_khi2(self, fonction_proba, nom_loi, indice_debut = None, indice_fin = None, degres_liberte=None): # si le nombre de degrés de liberté est omis alors on prend le nombre de valeurs différentes - 1 if degres_liberte is None: degres_liberte = len(valeurs)-1 # la série ne change pas : pas de réduction de la série de valeurs classes, effectifs_obs = (self.valeurs, self.effectifs) else: # sinon # regroupement des effectifs des valeurs mini et maxi de la série : classes = valeurs[indice_debut:indice_fin+1] classes, effectifs_obs = self.regrouper(indice_debut, indice_fin) # initialisation de la liste des effectifs attendus ou estimés effectifs_est=[] # moyenne pondérée de la série statistique m = self.moyenne_ponderee() # écart type de la série statistique s = self.ecart_type() # copie des paramètres du test actif dans l'attribut test_actif de la classe self.test_actif = Test('khi2', nom_loi, m, s, degres_liberte, None, None) # parcours des classes du milieu : classe d'indice 1 -> classe d'indice (n-1) for ci in classes[1:-1]: # calcul de l'effectif estimé pour xi : P(X = xi)*effectif_total ei = fonction_proba(ci)*self.effectif_total() effectifs_est.append(ei) # ajout de l'effetif à la liste # calcul de l'effectif estimé pour la 1re classe : P(X ≤ xi)*effectif_total e0 = fonction_proba(classes[0],cumulative=True)*self.effectif_total() # calcul de l'effectif estimé pour la dernière classe : (1 - P(X < xi))*effectif_total en = (1-fonction_proba(classes[-2],cumulative=True))*self.effectif_total() effectifs_est = [e0] + effectifs_est + [en] # résultat du test du khi carré : (T, pvalue) result_test = stats.chisquare(effectifs_obs, effectifs_est, ddof=degres_liberte) # copie de la valeur de T et de p-value dans self.test_actif self.test_actif.T = result_test[0] self.test_actif.pvalue = 1 - stats.chi2.cdf(self.test_actif.T, 1) # ou self.test_actif.pvalue = result_test[1] # copie du contenu de self.test_actif dans une variable resultat_test = self.test_actif self.test_actif = None # désactive le test # retourne le résultat du test sous la forme : Test(type_test='khi2', loi_probabilite='..', moyenne=.., ecart_type=.., degres_liberte=.., T=.., pvalue=..) return resultat_test def khi2(self, p, degres_liberte): # nombre de degrés de liberté du test df = degres_liberte # valeur du khi-carré pour p T = stats.chi2.ppf(p, df) return T def tracer(self, fonction_proba, nom_loi): # moyenne pondérée de la série statistique m = self.moyenne_ponderee() # écart type de la série statistique s = self.ecart_type() # copie des paramètres du test actif dans l'attribut test_actif de la classe self.test_actif = Test('Comparaison graphique', nom_loi, m, s, None, None, None) # liste des valeurs x = self.valeurs # liste des effectifs y1 = self.effectifs # initialisation de la liste des effectifs attendus y2=[] for xi in x: # parcours des valeurs # calcul de l'effectif de xi : P(X = xi)*effectif_total yi = fonction_proba(xi)*self.effectif_total() y2.append(yi) # ajout de l'effectif à la liste # trace les points de la série statistique (en bleu) et de la fonction de densité (en rouge) plt.scatter(x, y1, c = 'blue', marker = 'o' , s = 50, label = 'Série') plt.scatter(x, y2, c = 'red', marker = 'o' , s = 50, label = nom_loi) # relie les points de la série statistique (en bleu) et de la fonction de densité (en rouge) plt.plot(x, y1, c = 'grey') plt.plot(x, y2, c = 'grey') # ajout d'étiquettes sur l'axe des x et des y plt.xlabel('Valeurs') plt.ylabel('Effectifs') # ajoute une légende en haut à droite du graphique plt.legend(loc="upper right") # ajoute un titre plt.title("Ajustement d'une série de points à une " + nom_loi) self.test_actif = None # désactive le test # affiche le graphique obtenu plt.show() # Test d'adéquation d'une série statistique à une loi uniforme print("I. Test d'adéquation d'une série statistique à une loi uniforme : \n") # série de valeurs avec les effectifs valeurs = [1, 2, 3, 4, 5, 6] effectifs = [88, 109, 107, 94, 105, 97] # création d'un objet SerieStat avec la série de valeurs et les effectifs serie_stat = SerieStat(valeurs, effectifs) # nombre de degrés de liberté degres_liberte = 5 # on teste si la série de valeurs suit une loi uniforme discrète resultat_test = serie_stat.test_khi2(serie_stat.uniforme_discrete, "Loi uniforme discrète") # affiche le résultat du test du khi carré print(resultat_test) # prise en compte du risque α α = 0.05 # valeur complémentaire p = 1 - α # calcul du Khi2, valeur en deçà de laquelle on considère la série statistique comme conforme avec un risque α=(1-p). t = serie_stat.khi2(p, degres_liberte) print() # affiche la valeur de t telle que : P(T < t) = 0.95 print("P(T < " + str(t) + ") = " + str(p)) # Test d'adéquation d'une série statistique à une loi de Poisson print();print() print("II. Test d'adéquation d'une série statistique à une loi de Poisson : \n") # série de valeurs avec les effectifs valeurs = [0, 1, 2, 3, 4] effectifs = [31, 45, 16, 7, 1] # création d'un objet SerieStat avec la série de valeurs et les effectifs serie_stat = SerieStat(valeurs, effectifs) # nombre de degrés de liberté degres_liberte = 2 # on teste si la série de valeurs suit une loi de Poisson avec regroupement des effectifs des valeurs supérieurs ou égales à 3 dans une même classe resultat_test = serie_stat.test_khi2(serie_stat.poisson, "Loi de Poisson", indice_debut=0, indice_fin=3, degres_liberte = degres_liberte) # affiche le résultat du test du khi carré print(resultat_test) # prise en compte du risque α = 0.05 # valeur complémentaire p = 1 - α # calcul du Khi2, valeur en deçà de laquelle on considère la série statistique comme conforme avec un risque α. t = serie_stat.khi2(p, degres_liberte) print() # affiche la valeur de t telle que : P(T < t) = 0.95 print("P(T < " + str(t) + ") = " + str(p)) # Trace les points de la série de valeurs comparés à ceux donnés par la fonction de masse de la loi de Poisson serie_stat.tracer(serie_stat.poisson,'Loi de Poisson')
Le code affiche :
Puis, génère le graphique à partir de la série statistique et de la fonction de probabilité :
VI. Conclusion
Après avoir montré comment utiliser le test du khi carré pour vérifier qu'une série de données statistiques suit bien une loi de probabilité, nous avons pu implémenter ce test à l'aide de la classe SerieStat et de ses différentes méthodes.
Chacun pourra ensuite ajouter de nouvelles méthodes à cette classe pour par exemple effectuer des tests en utilisant d'autres lois de probabilité.
Sources :
https://fr.wikipedia.org/wiki/Test_du_%CF%87%C2%B2
https://fr.wikipedia.org/wiki/Valeur_p
https://fr.wikipedia.org/wiki/Degr%C...(statistiques)
https://fr.wikipedia.org/wiki/Loi_un..._discr%C3%A8te
https://fr.wikipedia.org/wiki/Loi_uniforme_continue
https://fr.wikipedia.org/wiki/Loi_de_Poisson
https://docs.scipy.org/doc/scipy/reference/stats.html
https://docs.python.org/fr/3/tutorial/classes.html