AutoStatLib 0.1.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,554 @@
1
+ import numpy as np
2
+ from scipy.stats import ttest_rel, ttest_ind, ttest_1samp, wilcoxon, mannwhitneyu, f_oneway, kruskal, friedmanchisquare, shapiro, norm
3
+
4
+
5
+ class __StatisticalTests():
6
+ '''
7
+ Statistical tests mixin
8
+ '''
9
+
10
+ def anova(self):
11
+ stat, p_value = f_oneway(*self.data)
12
+ if self.tails == 1 and p_value > 0.5:
13
+ p_value /= 2
14
+ # if self.tails == 1:
15
+ # p_value /= 2
16
+ self.test_name = 'ANOVA'
17
+ self.test_id = 'anova'
18
+ self.paired = False
19
+ self.test_stat = stat
20
+ self.p_value = p_value
21
+
22
+ def friedman_test(self):
23
+ stat, p_value = friedmanchisquare(*self.data)
24
+ self.test_name = 'Friedman test'
25
+ self.test_id = 'friedman'
26
+ self.paired = True
27
+ self.test_stat = stat
28
+ self.p_value = p_value
29
+
30
+ def kruskal_wallis_test(self):
31
+ stat, p_value = kruskal(*self.data)
32
+ self.test_name = 'Kruskal-Wallis test'
33
+ self.test_id = 'kruskal_wallis'
34
+ self.paired = False
35
+ self.test_stat = stat
36
+ self.p_value = p_value
37
+
38
+ def mann_whitney_u_test(self):
39
+ stat, p_value = mannwhitneyu(
40
+ self.data[0], self.data[1], alternative='two-sided' if self.tails == 2 else 'greater')
41
+ # if self.tails == 1:
42
+ # p_value /= 2
43
+ self.test_name = 'Mann-Whitney U test'
44
+ self.test_id = 'mann_whitney'
45
+ self.paired = False
46
+ self.test_stat = stat
47
+ self.p_value = p_value
48
+
49
+ def t_test_independend(self):
50
+ t_stat, t_p_value = ttest_ind(
51
+ self.data[0], self.data[1])
52
+
53
+ if self.tails == 1:
54
+ t_p_value /= 2
55
+
56
+ self.test_name = 't-test for independend samples'
57
+ self.test_id = 't_test_independend'
58
+ self.paired = False
59
+ self.test_stat = t_stat
60
+ self.p_value = t_p_value
61
+
62
+ def t_test_paired(self):
63
+ t_stat, t_p_value = ttest_rel(
64
+ self.data[0], self.data[1])
65
+
66
+ if self.tails == 1:
67
+ t_p_value /= 2
68
+
69
+ self.test_name = 't-test for paired samples'
70
+ self.test_id = 't_test_paired'
71
+ self.paired = True
72
+ self.test_stat = t_stat
73
+ self.p_value = t_p_value
74
+
75
+ def t_test_single_sample(self):
76
+ if self.popmean == None:
77
+ self.popmean = 0
78
+ self.AddWarning('no_pop_mean_set')
79
+ t_stat, t_p_value = ttest_1samp(self.data[0], self.popmean)
80
+ if self.tails == 1:
81
+ t_p_value /= 2
82
+
83
+ self.test_name = 'Single-sample t-test'
84
+ self.test_id = 't_test_single_sample'
85
+ self.paired = False
86
+ self.test_stat = t_stat
87
+ self.p_value = t_p_value
88
+
89
+ def wilcoxon_single_sample(self):
90
+ if self.popmean == None:
91
+ self.popmean = 0
92
+ self.AddWarning('no_pop_mean_set')
93
+ data = [i - self.popmean for i in self.data[0]]
94
+ w_stat, w_p_value = wilcoxon(data)
95
+ if self.tails == 1:
96
+ p_value /= 2
97
+
98
+ self.test_name = 'Wilcoxon signed-rank test for single sample'
99
+ self.test_id = 'wilcoxon_single_sample'
100
+ self.paired = False
101
+ self.test_stat = w_stat
102
+ self.p_value = w_p_value
103
+
104
+ def wilcoxon(self):
105
+ stat, p_value = wilcoxon(self.data[0], self.data[1])
106
+ if self.tails == 1:
107
+ p_value /= 2
108
+ self.test_name = 'Wilcoxon signed-rank test'
109
+ self.test_id = 'wilcoxon'
110
+ self.paired = True
111
+ self.test_stat = stat
112
+ self.p_value = p_value
113
+
114
+
115
+ class __NormalityTests():
116
+ '''
117
+ Normality tests mixin
118
+ '''
119
+
120
+ def check_normality(self, data):
121
+ # Shapiro-Wilk Test
122
+ sw_stat, sw_p_value = shapiro(data)
123
+ if sw_p_value > 0.05:
124
+ return True, 'Shapiro-Wilk'
125
+
126
+ # Lilliefors Test (Kolmogorov-Smirnov test)
127
+ lilliefors_stat, lilliefors_p_value = self.lilliefors_test(data)
128
+ if lilliefors_p_value > 0.05:
129
+ return True, 'Lilliefors'
130
+
131
+ return False, 'Not normal'
132
+
133
+ def lilliefors_test(self, data):
134
+ ''' Lilliefors Test (Kolmogorov-Smirnov test) '''
135
+ data = np.sort(data)
136
+ n = len(data)
137
+ mean = np.mean(data)
138
+ std = np.std(data, ddof=1)
139
+ cdf_values = norm.cdf(data, loc=mean, scale=std)
140
+ empirical_cdf = np.arange(1, n + 1) / n
141
+ D_plus = np.max(empirical_cdf - cdf_values)
142
+ D_minus = np.max(cdf_values - (np.arange(n) / n))
143
+ D = max(D_plus, D_minus)
144
+
145
+ # Use critical values from the Lilliefors table
146
+ # (two-sided test, approximations for large n)
147
+ critical_values = {
148
+ 100: 0.072,
149
+ 1000: 0.024,
150
+ 5000: 0.018,
151
+ 10000: 0.015,
152
+ np.inf: 0.01
153
+ }
154
+
155
+ for size, critical_value in critical_values.items():
156
+ if n <= size:
157
+ return D, 1 - critical_value if D < critical_value else 0
158
+
159
+ return D, 0
160
+
161
+
162
+ class __TextFormatting():
163
+ '''
164
+ Text formatting mixin
165
+ '''
166
+
167
+ def autospace(self, elements_list, space, delimiter=' ') -> str:
168
+ output = ''
169
+ for i, element in enumerate(elements_list):
170
+ if i == len(elements_list):
171
+ output += element
172
+ else:
173
+ output += element + (space-len(element))*delimiter
174
+ return output
175
+
176
+ def print_groups(self, space=24, max_length=15):
177
+ self.log('')
178
+ # Get the number of groups (rows) and the maximum length of rows
179
+ data = self.data
180
+ num_groups = len(data)
181
+ group_longest = max(len(row) for row in data)
182
+
183
+ # Print the header
184
+ header = [f'Group {i+1}' for i in range(num_groups)]
185
+ line = [''*7]
186
+ self.log(self.autospace(header, space))
187
+ self.log(self.autospace(line, space))
188
+
189
+ # Print each column with a placeholder if longer than max_length
190
+ for i in range(group_longest):
191
+ row_values = []
192
+ all_values_empty = True
193
+ for row in data:
194
+ if len(row) > max_length:
195
+ if i < max_length:
196
+ row_values.append(str(row[i]))
197
+ all_values_empty = False
198
+ elif i == max_length:
199
+ row_values.append(f'[{len(row) - max_length} more]')
200
+ all_values_empty = False
201
+ else:
202
+ continue
203
+ else:
204
+ if i < len(row):
205
+ row_values.append(str(row[i]))
206
+ all_values_empty = False
207
+ else:
208
+ row_values.append('')
209
+ if all_values_empty:
210
+ break
211
+ self.log(self.autospace(row_values, space))
212
+
213
+ def make_stars(self) -> int:
214
+ p = self.p_value.item()
215
+ if p is not None:
216
+ if p < 0.0001:
217
+ return 4
218
+ if p < 0.001:
219
+ return 3
220
+ elif p < 0.01:
221
+ return 2
222
+ elif p < 0.05:
223
+ return 1
224
+ else:
225
+ return 0
226
+ return 0
227
+
228
+ def make_p_value_printed(self) -> str:
229
+ p = self.p_value.item()
230
+ if p is not None:
231
+ if p > 0.99:
232
+ return 'p>0.99'
233
+ elif p >= 0.01:
234
+ return f'p={p:.2g}'
235
+ elif p >= 0.001:
236
+ return f'p={p:.2g}'
237
+ elif p >= 0.0001:
238
+ return f'p={p:.1g}'
239
+ elif p < 0.0001:
240
+ return 'p<0.0001'
241
+ else:
242
+ return 'N/A'
243
+ return 'N/A'
244
+
245
+ def print_results(self):
246
+ self.log('\n\nResults: \n')
247
+ for i in self.results:
248
+ shift = 27 - len(i)
249
+ if i == 'Warnings':
250
+ self.log(i, ':', ' ' * shift, len(self.results[i]))
251
+ elif i == 'Samples':
252
+ pass
253
+ else:
254
+ self.log(i, ':', ' ' * shift, self.results[i])
255
+
256
+ def create_results_dict(self) -> dict:
257
+
258
+ self.stars_int = self.make_stars()
259
+ self.stars_str = '*' * self.stars_int if self.stars_int else 'ns'
260
+
261
+ return {
262
+ 'p-value': self.make_p_value_printed(),
263
+ 'Significance(p<0.05)': True if self.p_value.item() < 0.05 else False,
264
+ 'Stars_Printed': self.stars_str,
265
+ 'Test_Name': self.test_name,
266
+ 'Groups_Compared': self.n_groups,
267
+ 'Population_Mean': self.popmean if self.n_groups == 1 else 'N/A',
268
+ 'Data_Normaly_Distributed': self.parametric,
269
+ 'Parametric_Test_Applied': True if self.test_id in self.test_ids_parametric else False,
270
+ 'Paired_Test_Applied': self.paired,
271
+ 'Tails': self.tails,
272
+ 'p-value_exact': self.p_value.item(),
273
+ 'Stars': self.stars_int,
274
+ # 'Stat_Value': self.test_stat.item(),
275
+ 'Warnings': self.warnings,
276
+ 'Groups_N': [len(self.data[i]) for i in range(len(self.data))],
277
+ 'Groups_Median': [np.median(self.data[i]).item() for i in range(len(self.data))],
278
+ 'Groups_Mean': [np.mean(self.data[i]).item() for i in range(len(self.data))],
279
+ 'Groups_SD': [np.std(self.data[i]).item() for i in range(len(self.data))],
280
+ 'Groups_SE': [np.std(self.data[i]).item() / np.sqrt(len(self.data)).item() for i in range(len(self.data))],
281
+ # actually returns list of lists of numpy dtypes of float64, next make it return regular floats:
282
+ 'Samples': self.data,
283
+ }
284
+
285
+ def log(self, *args, **kwargs):
286
+ message = ' '.join(map(str, args))
287
+ # print(message, **kwargs)
288
+ self.summary += '\n' + message
289
+
290
+ def AddWarning(self, warning_id):
291
+ message = self.warning_ids_all[warning_id]
292
+ self.log(message)
293
+ self.warnings.append(message)
294
+
295
+
296
+ class __InputFormatting():
297
+ def floatify_recursive(self, data):
298
+ if isinstance(data, list):
299
+ # Recursively process sublists and filter out None values
300
+ processed_list = [self.floatify_recursive(item) for item in data]
301
+ return [item for item in processed_list if item is not None]
302
+ else:
303
+ try:
304
+ # Try to convert the item to float
305
+ return np.float64(data)
306
+ except (ValueError, TypeError):
307
+ # If conversion fails, replace with None
308
+ self.warning_flag_non_numeric_data = True
309
+ return None
310
+
311
+
312
+ class StatisticalAnalysis(__StatisticalTests, __NormalityTests, __TextFormatting, __InputFormatting):
313
+ '''
314
+ The main class
315
+ *documentation placeholder*
316
+
317
+ '''
318
+
319
+ def __init__(self,
320
+ groups_list,
321
+ paired=False,
322
+ tails=2,
323
+ popmean=None,
324
+ verbose=True):
325
+ self.results = None
326
+ self.groups_list = groups_list
327
+ self.paired = paired
328
+ self.tails = tails
329
+ self.popmean = popmean
330
+ self.verbose = verbose
331
+ self.n_groups = len(self.groups_list)
332
+ self.warning_flag_non_numeric_data = False
333
+ self.summary = ''
334
+ self.test_ids_parametric = ['anova',
335
+ 't_test_independend',
336
+ 't_test_paired',
337
+ 't_test_single_sample',]
338
+ self.test_ids_all = [ # in aplhabetical order
339
+ 'anova',
340
+ 'friedman',
341
+ 'kruskal_wallis',
342
+ 'mann_whitney',
343
+ 't_test_independend',
344
+ 't_test_paired',
345
+ 't_test_single_sample',
346
+ 'wilcoxon',
347
+ 'wilcoxon_single_sample',
348
+ ]
349
+ self.warning_ids_all = {
350
+ 'not-numeric': '\nWarning: Non-numeric data was found in input and ignored.\n Make sure the input data is correct to get the correct results\n',
351
+ 'param_test_with_non-normal_data': '\nWarning: Parametric test was manualy chosen for Not-Normaly distributed data.\n The results might be skewed. \n Please, run non-parametric test or preform automatic test selection.\n',
352
+ 'non-param_test_with_normal_data': '\nWarning: Non-Parametric test was manualy chosen for Normaly distributed data.\n The results might be skewed. \n Please, run parametric test or preform automatic test selection.\n',
353
+ 'no_pop_mean_set': '\nWarning: No Population Mean was set up for single-sample test, used default 0 value.\n The results might be skewed. \n Please, set the Population Mean and run the test again.\n',
354
+ }
355
+
356
+ def __run_test(self, test='auto'):
357
+
358
+ # reset values from previous tests
359
+ self.results = None
360
+ self.warnings = []
361
+ self.normals = []
362
+ self.methods = []
363
+ self.test_name = None
364
+ self.test_id = None
365
+ self.test_stat = None
366
+ self.p_value = None
367
+
368
+ self.log('\n' + '-'*67)
369
+ self.log('Statistical analysis initiated for data in {} groups\n'.format(
370
+ len(self.groups_list)))
371
+
372
+ # adjusting input data type
373
+ self.data = self.floatify_recursive(self.groups_list)
374
+ if self.warning_flag_non_numeric_data:
375
+ self.AddWarning('not-numeric')
376
+
377
+ # delete the empty cols from input
378
+ self.data = [col for col in self.data if any(
379
+ x is not None for x in col)]
380
+
381
+ # User input assertion block
382
+ try:
383
+ assert self.tails in [1, 2], 'Tails parameter can be 1 or 2 only'
384
+ assert test in self.test_ids_all or test == 'auto', 'Wrong test id choosen, ensure you called correct function'
385
+ assert not (self.n_groups > 1
386
+ and (test == 't_test_single_sample'
387
+ or test == 'wilcoxon_single_sample')), 'Only one group of data must be given for single-group tests'
388
+ assert all(len(
389
+ group) > 2 for group in self.data), 'Each group must contain at least three values'
390
+ assert not (self.paired == True and not all(len(lst) == len(
391
+ self.data[0]) for lst in self.data)), 'Paired groups must be the same length'
392
+ assert not (test == 'friedman' and not all(len(lst) == len(
393
+ self.data[0]) for lst in self.data)), 'Paired groups must be the same length for Friedman Chi Square test'
394
+ assert not (test == 't_test_paired' and not all(len(lst) == len(
395
+ self.data[0]) for lst in self.data)), 'Paired groups must be the same length for Paired t-test'
396
+ assert not (test == 'wilcoxon' and not all(len(lst) == len(
397
+ self.data[0]) for lst in self.data)), 'Paired groups must be the same length for Wilcoxon signed-rank test'
398
+ assert not (test == 'friedman' and self.n_groups <
399
+ 3), 'At least three groups of data must be given for 3-groups tests'
400
+ assert not ((test == 'anova'
401
+ or test == 'kruskal_wallis') and self.n_groups < 2), 'At least two groups of data must be given for ANOVA or Kruskal Wallis tests'
402
+ assert not ((test == 'wilcoxon'
403
+ or test == 't_test_independend'
404
+ or test == 't_test_paired'
405
+ or test == 'mann_whitney')
406
+ and self.n_groups != 2), 'Only two groups of data must be given for 2-groups tests'
407
+ except AssertionError as error:
408
+ self.log('\nTest :', test)
409
+ self.log('Error :', error)
410
+ self.log('-'*67 + '\n')
411
+ print(self.summary)
412
+ return
413
+
414
+ # Print the data
415
+ self.print_groups()
416
+
417
+ # Normality tests
418
+ self.log('\n\nNormality checked by both Shapiro-Wilk and Lilliefors tests')
419
+ self.log('Group data is normal if at least one results is positive:\n')
420
+ for i, data in enumerate(self.data):
421
+ normal, method = self.check_normality(data)
422
+ self.normals.append(normal)
423
+ self.methods.append(method)
424
+ self.log(
425
+ f' Group {i+1}: disrtibution is {"normal" if normal else "not normal"}')
426
+ self.parametric = all(self.normals)
427
+
428
+ # print test choosen
429
+ self.log('\n\nInput:\n')
430
+ self.log('Data Normaly Distributed: ', self.parametric)
431
+ self.log('Paired Groups: ', self.paired)
432
+ self.log('Groups: ', self.n_groups)
433
+ self.log('Test chosen by user: ', test)
434
+
435
+ # Wrong test Warnings
436
+ if not test == 'auto' and not self.parametric and test in self.test_ids_parametric:
437
+ self.AddWarning('param_test_with_non-normal_data')
438
+ if not test == 'auto' and self.parametric and not test in self.test_ids_parametric:
439
+ self.AddWarning('non-param_test_with_normal_data')
440
+
441
+ if test == 'anova':
442
+ self.anova()
443
+ elif test == 'friedman':
444
+ self.friedman_test()
445
+ elif test == 'kruskal_wallis':
446
+ self.kruskal_wallis_test()
447
+ elif test == 'mann_whitney':
448
+ self.mann_whitney_u_test()
449
+ elif test == 't_test_independend':
450
+ self.t_test_independend()
451
+ elif test == 't_test_paired':
452
+ self.t_test_paired()
453
+ elif test == 't_test_single_sample':
454
+ self.t_test_single_sample()
455
+ elif test == 'wilcoxon':
456
+ self.wilcoxon()
457
+ elif test == 'wilcoxon_single_sample':
458
+ self.wilcoxon_single_sample()
459
+ else:
460
+ self.log('Automatic test selection preformed.')
461
+ self.__auto()
462
+
463
+ # print the results
464
+ self.results = self.create_results_dict()
465
+ self.print_results()
466
+ self.log(
467
+ '\n\nResults above are accessible as a dictionary via GetResult() method')
468
+ self.log('-'*67 + '\n')
469
+
470
+ # print the results to console:
471
+ if self.verbose == True:
472
+ print(self.summary)
473
+
474
+ def __auto(self):
475
+
476
+ if self.n_groups == 2:
477
+ if self.paired:
478
+ if self.parametric:
479
+ return self.t_test_paired()
480
+ else:
481
+ return self.wilcoxon()
482
+ else:
483
+ if self.parametric:
484
+ return self.t_test_independend()
485
+ else:
486
+ return self.mann_whitney_u_test()
487
+ elif self.n_groups == 1:
488
+ if self.parametric:
489
+ return self.t_test_single_sample()
490
+ else:
491
+ return self.wilcoxon_single_sample()
492
+ else:
493
+ if self.paired:
494
+ return self.friedman_test()
495
+ else:
496
+ if self.parametric:
497
+ return self.anova()
498
+ else:
499
+ return self.kruskal_wallis_test()
500
+
501
+ # public methods:
502
+ def RunAuto(self):
503
+ self.__run_test(test='auto')
504
+
505
+ def RunManual(self, test):
506
+ self.__run_test(test)
507
+
508
+ def RunAnova(self):
509
+ self.__run_test(test='anova')
510
+
511
+ def RunFriedman(self):
512
+ self.__run_test(test='friedman')
513
+
514
+ def RunKruskalWallis(self):
515
+ self.__run_test(test='kruskal_wallis')
516
+
517
+ def RunMannWhitney(self):
518
+ self.__run_test(test='mann_whitney')
519
+
520
+ def RunTtest(self):
521
+ self.__run_test(test='t_test_independend')
522
+
523
+ def RunTtestPaired(self):
524
+ self.__run_test(test='t_test_paired')
525
+
526
+ def RunTtestSingleSample(self):
527
+ self.__run_test(test='t_test_single_sample')
528
+
529
+ def RunWilcoxonSingleSample(self):
530
+ self.__run_test(test='wilcoxon_single_sample')
531
+
532
+ def RunWilcoxon(self):
533
+ self.__run_test(test='wilcoxon')
534
+
535
+ def GetResult(self):
536
+ if self.results:
537
+ return self.results
538
+ else:
539
+ self.__run_test(test='auto')
540
+ return self.results
541
+
542
+ def GetSummary(self):
543
+ if self.results:
544
+ return self.summary
545
+ else:
546
+ self.__run_test(test='auto')
547
+ return self.summary
548
+
549
+ def PrintSummary(self):
550
+ print(self.summary)
551
+
552
+
553
+ if __name__ == '__main__':
554
+ print('\nThis package works as an imported module only\n')
@@ -0,0 +1,2 @@
1
+ from .AutoStatLib import StatisticalAnalysis
2
+ from ._version import __version__
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env python
2
+ from .AutoStatLib import StatisticalAnalysis
3
+ from ._version import __version__
4
+
5
+ if __name__ == '__main__':
6
+ print('\nThis package works as an imported module only\n')
@@ -0,0 +1,2 @@
1
+ # AutoStatLib package version:
2
+ __version__ = "0.1.2"