benchstats 3.3.2__tar.gz → 3.4.0__tar.gz

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.
Files changed (29) hide show
  1. {benchstats-3.3.2 → benchstats-3.4.0}/CHANGELOG.md +16 -0
  2. {benchstats-3.3.2/src/benchstats.egg-info → benchstats-3.4.0}/PKG-INFO +1 -1
  3. {benchstats-3.3.2 → benchstats-3.4.0}/pyproject.toml +1 -1
  4. {benchstats-3.3.2 → benchstats-3.4.0}/src/benchstats/compare.py +80 -38
  5. {benchstats-3.3.2 → benchstats-3.4.0}/src/benchstats/qbench.py +108 -35
  6. {benchstats-3.3.2 → benchstats-3.4.0}/src/benchstats/render.py +14 -12
  7. {benchstats-3.3.2 → benchstats-3.4.0/src/benchstats.egg-info}/PKG-INFO +1 -1
  8. {benchstats-3.3.2 → benchstats-3.4.0}/tests/test_compare.py +5 -2
  9. {benchstats-3.3.2 → benchstats-3.4.0}/tests/test_qbench.py +58 -17
  10. {benchstats-3.3.2 → benchstats-3.4.0}/LICENSE +0 -0
  11. {benchstats-3.3.2 → benchstats-3.4.0}/MANIFEST.in +0 -0
  12. {benchstats-3.3.2 → benchstats-3.4.0}/README.md +0 -0
  13. {benchstats-3.3.2 → benchstats-3.4.0}/docs/imgs/simple1.svg +0 -0
  14. {benchstats-3.3.2 → benchstats-3.4.0}/docs/imgs/simple2.svg +0 -0
  15. {benchstats-3.3.2 → benchstats-3.4.0}/docs/imgs/simple3.svg +0 -0
  16. {benchstats-3.3.2 → benchstats-3.4.0}/docs/imgs/simple4.svg +0 -0
  17. {benchstats-3.3.2 → benchstats-3.4.0}/docs/imgs/simple4_bad.svg +0 -0
  18. {benchstats-3.3.2 → benchstats-3.4.0}/setup.cfg +0 -0
  19. {benchstats-3.3.2 → benchstats-3.4.0}/src/benchstats/__init__.py +0 -0
  20. {benchstats-3.3.2 → benchstats-3.4.0}/src/benchstats/__main__.py +0 -0
  21. {benchstats-3.3.2 → benchstats-3.4.0}/src/benchstats/cli_parser.py +0 -0
  22. {benchstats-3.3.2 → benchstats-3.4.0}/src/benchstats/common.py +0 -0
  23. {benchstats-3.3.2 → benchstats-3.4.0}/src/benchstats/parser_GbenchJson.py +0 -0
  24. {benchstats-3.3.2 → benchstats-3.4.0}/src/benchstats/parsers.py +0 -0
  25. {benchstats-3.3.2 → benchstats-3.4.0}/src/benchstats.egg-info/SOURCES.txt +0 -0
  26. {benchstats-3.3.2 → benchstats-3.4.0}/src/benchstats.egg-info/dependency_links.txt +0 -0
  27. {benchstats-3.3.2 → benchstats-3.4.0}/src/benchstats.egg-info/entry_points.txt +0 -0
  28. {benchstats-3.3.2 → benchstats-3.4.0}/src/benchstats.egg-info/requires.txt +0 -0
  29. {benchstats-3.3.2 → benchstats-3.4.0}/src/benchstats.egg-info/top_level.txt +0 -0
@@ -1,3 +1,19 @@
1
+ # 3.4.0
2
+ - `qbench.showBench()` can take benchmark results as a dict of per-name 2D `[reps, iters]` arrays
3
+ (with `bm_names=None` and a non-empty `alt_delimiter`), not only a single 3D numpy stack.
4
+ - `qbench.showBench()` gained `allow_inplace_reshuffle`: opt in to allow in-place shuffling/copy
5
+ avoidance when bootstrapping or normalizing inputs, instead of always working on a private copy.
6
+ - Restored default value for `qbench.showBench(pvalue_stats_bootstrap=)` to 1000.
7
+ - `CompareStatsResult` now exposes `comparisons` (which original benchmark names each row compares)
8
+ and `comparison_indices` (which rows of the stacked `showBench` input they correspond to; absent
9
+ for dict-input mode), for easier downstream automation.
10
+
11
+ # 3.3.3
12
+ - breaking change, though unlikely important for anyone: due to T-test also being affected by
13
+ bad data, had to rename `compareStats()`'s parameter `brunnermunzel_workaround` to `edge_cases_workaround`
14
+ and extended its effect on t-test.
15
+ - Fix `_at_least_one_differs` flag handling in `CompareStatsResult::updatePvalStats()`.
16
+
1
17
  # 3.3.1-.2
2
18
  - Changed the order of default metrics of `qbench.showBench()` to make `mean` the leading metric, as
3
19
  it's typically the most correct one.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: benchstats
3
- Version: 3.3.2
3
+ Version: 3.4.0
4
4
  Summary: Statistical Testing for Benchmark Results Comparison
5
5
  Author-email: Aleksei Rechinskii <5dpea9nhd@mozmail.com>
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "benchstats"
7
- version = "3.3.2"
7
+ version = "3.4.0"
8
8
  description = "Statistical Testing for Benchmark Results Comparison"
9
9
  readme = "README.md"
10
10
  authors = [{ name = "Aleksei Rechinskii", email = "5dpea9nhd@mozmail.com" }]
@@ -39,7 +39,7 @@ class BmCompResult(
39
39
  def __new__(
40
40
  cls,
41
41
  res: str,
42
- pval: float | int,
42
+ pvalue: float | int,
43
43
  v0: float | int | np.ndarray,
44
44
  v1: float | int | np.ndarray,
45
45
  siz0: int,
@@ -52,8 +52,8 @@ class BmCompResult(
52
52
  > when set1 is stochastically greater than set2,
53
53
  ~ when set1 is not stochastically less or greater than set2
54
54
 
55
- - pval/pvalue is a pvalue associated with less or greater comparison result, or a minimum of
56
- pvalues for less/greater comparison
55
+ - pvalue is a p-value associated with less or greater comparison result, or a minimum of
56
+ p-values for less/greater comparison
57
57
 
58
58
  - v0/val_set0 and v1/val_set1 are either representative values (mean) of a corresponding
59
59
  set, or the whole set iself, depending on `store_sets` flag value of compareStats()
@@ -62,10 +62,10 @@ class BmCompResult(
62
62
  """
63
63
 
64
64
  # assert isinstance(rel, bool) and isinstance(pval, (float, np.floating))
65
- if isinstance(pval, int):
66
- pval = float(pval)
67
- assert isinstance(pval, kAllowedFpTypes)
68
- assert 0 <= pval <= 1
65
+ if isinstance(pvalue, int):
66
+ pvalue = float(pvalue)
67
+ assert isinstance(pvalue, kAllowedFpTypes)
68
+ assert 0 <= pvalue <= 1
69
69
  if isinstance(v0, int):
70
70
  v0 = float(v0)
71
71
  if isinstance(v1, int):
@@ -80,7 +80,7 @@ class BmCompResult(
80
80
  return super().__new__(
81
81
  cls,
82
82
  res,
83
- float(pval),
83
+ float(pvalue),
84
84
  v0 if isinstance(v0, np.ndarray) else float(v0),
85
85
  v1 if isinstance(v1, np.ndarray) else float(v1),
86
86
  siz0,
@@ -95,10 +95,12 @@ class CompareStatsResult:
95
95
  method: str,
96
96
  alpha: float,
97
97
  at_least_one_differs: bool,
98
+ comparisons: dict[str, tuple[str, str]],
98
99
  ):
99
100
  """Constructor to verify initialization correctness.
100
- - results field is a mapping {benchmark_name -> {metric_name -> CompResult}}. Keys of the
101
+ - results is a mapping {benchmark_name -> {metric_name -> CompResult}}. Keys of the
101
102
  top-level dict are common between the data sources sg0 and sg1.
103
+ - comparisons is a mapping {benchmark_name -> (bm_name_in_set0, bm_name_in_set1)}
102
104
  """
103
105
  assert isinstance(results, dict) and isinstance(method, str) and method
104
106
  assert isinstance(alpha, kAllowedFpTypes)
@@ -107,6 +109,8 @@ class CompareStatsResult:
107
109
  self._method: str = method
108
110
  self._alpha: float = float(alpha)
109
111
  self._at_least_one_differs = bool(at_least_one_differs)
112
+ self._comparisons: dict[str, tuple[str, str]] = comparisons
113
+ self._comparison_indices: dict[str, tuple[int, int]] | None = None
110
114
  # {benchmark_name -> {metric_name -> {result_type -> list of pvalues}}}
111
115
  self._pval_stats: None | dict[str, dict[str, dict[str, list[float]]]] = None
112
116
 
@@ -122,10 +126,33 @@ class CompareStatsResult:
122
126
  def alpha(self) -> float:
123
127
  return self._alpha
124
128
 
129
+ @property
130
+ def comparisons(self) -> dict[str, tuple[str, str]]:
131
+ """Returns a mapping between a benchmark name in the results and a tuple of names of the
132
+ original data in the original data sets"""
133
+ return self._comparisons
134
+
125
135
  @property
126
136
  def at_least_one_differs(self) -> bool:
137
+ """Returns True if at least one benchmark/metric comparison result is different from "~"
138
+ (no difference) for any metric from `main_metrics` list (or for any metric if `main_metrics`
139
+ is None)"""
127
140
  return self._at_least_one_differs
128
141
 
142
+ @property
143
+ def comparison_indices(self) -> dict[str, tuple[int, int]] | None:
144
+ """Returns a mapping between a benchmark name in the results and a tuple of indices of the
145
+ original data in the original data sets"""
146
+ return self._comparison_indices
147
+
148
+ def setComparisonIndices(self, comparison_indices: dict[str, tuple[int, int]]):
149
+ """Sets a mapping between a benchmark name in the results and a tuple of indices of the
150
+ original data of the original data sets (when qbench's form of benchmarks is used)"""
151
+ # assert isinstance(comparison_indices, dict)
152
+ # assert all(isinstance(k, str) and isinstance(v, tuple) and len(v) == 2 and all(isinstance(i, int) for i in v) for k, v in comparison_indices.items())
153
+ # assert frozenset(comparison_indices.keys()) == frozenset(self._comparisons.keys())
154
+ self._comparison_indices = comparison_indices
155
+
129
156
  def getMetrics(self) -> tuple[str]:
130
157
  return tuple(next(iter(self._results.values())).keys())
131
158
 
@@ -134,13 +161,13 @@ class CompareStatsResult:
134
161
 
135
162
  def areAllSame(self) -> bool:
136
163
  """Tests if all benchmarks over all metrics compare same"""
137
- return all([
164
+ return all(
138
165
  "~" == cr.result for bm_res in self._results.values() for cr in bm_res.values()
139
- ])
166
+ )
140
167
 
141
168
  def areMetricsSame(self, metrics: Iterable[str]) -> bool:
142
169
  """Tests if all benchmarks over specified metrics compare same"""
143
- return all(["~" == bm_res[m].result for bm_res in self._results.values() for m in metrics])
170
+ return all("~" == bm_res[m].result for bm_res in self._results.values() for m in metrics)
144
171
 
145
172
  @property
146
173
  def pval_stats_available(self) -> bool:
@@ -162,6 +189,8 @@ class CompareStatsResult:
162
189
  assert isinstance(other, CompareStatsResult)
163
190
  assert other.method == self.method and other.alpha == self.alpha
164
191
  assert self._results.keys() == other._results.keys()
192
+ assert self._comparisons == other._comparisons
193
+ self._at_least_one_differs = self._at_least_one_differs or other._at_least_one_differs
165
194
  for bm, omdict in other._results.items():
166
195
  mdict = self._results[bm]
167
196
  assert mdict.keys() == omdict.keys()
@@ -182,7 +211,7 @@ def poolBenchmarks(
182
211
  sg0: dict[str, dict[str, Iterable[float]]],
183
212
  sg1: dict[str, dict[str, Iterable[float]]] | None,
184
213
  logger: LoggingConsole | None = None,
185
- ) -> dict[str, dict[str, dict[str, Iterable[float]]]]:
214
+ ) -> tuple[dict[str, dict[str, dict[str, Iterable[float]]]], dict[str, dict[str, str]]]:
186
215
  """Using the specified delimiter divides each benchmark name into (the longest possible) common
187
216
  part and an alternative name part, merges the benchmarks from one or two sets of benchmarks into
188
217
  a single pool represented by a dictionary where a key specifies common part of benchmark name
@@ -192,6 +221,9 @@ def poolBenchmarks(
192
221
  If a benchmark name doesn't contain the delimiter, then its common name is assumed to be a whole
193
222
  benchmark name, and its alternative name is assumed to either "0" or "1" depending on the
194
223
  benchmark set (sg0 or sg1) it belongs to.
224
+
225
+ Returns a mapping {common_name -> {alternative_name -> {metric_name -> values}}}, and a mapping
226
+ of original names {common_name -> {alternative_name -> original_name}}
195
227
  """
196
228
  assert isinstance(alt_delimiter, str) and len(alt_delimiter) > 0
197
229
  assert isinstance(sg0, dict) and (isinstance(sg1, dict) or sg1 is None)
@@ -212,12 +244,15 @@ def poolBenchmarks(
212
244
  else:
213
245
  return bm_name, pfx
214
246
 
247
+ pool = {}
248
+ original_names = {}
249
+
215
250
  def _do_pool(
216
- pool: dict[str, dict[str, dict[str, Iterable[float]]]],
217
251
  sg: dict[str, dict[str, Iterable[float]]],
218
252
  pfx: str,
219
253
  ):
220
254
  """Merges the benchmarks from one set of benchmarks into a pool"""
255
+ nonlocal pool, original_names
221
256
  for bm_name, metrics in sg.items():
222
257
  common_name, alt_name = splitName(bm_name, pfx)
223
258
  cmn_bm = pool.setdefault(common_name, {})
@@ -229,15 +264,15 @@ def poolBenchmarks(
229
264
  )
230
265
  continue
231
266
  cmn_bm[alt_name] = metrics
267
+ original_names.setdefault(common_name, {})[alt_name] = bm_name
232
268
 
233
- pool = {}
234
269
  if sg1 is None:
235
- _do_pool(pool, sg0, "")
270
+ _do_pool(sg0, "")
236
271
  else:
237
- _do_pool(pool, sg0, "0")
238
- _do_pool(pool, sg1, "1")
272
+ _do_pool(sg0, "0")
273
+ _do_pool(sg1, "1")
239
274
 
240
- return pool
275
+ return pool, original_names
241
276
 
242
277
 
243
278
  def compareStats(
@@ -246,10 +281,10 @@ def compareStats(
246
281
  alt_delimiter: str | None = None,
247
282
  method: str = next(iter(kMethods.keys())),
248
283
  alpha: float = kDefaultAlpha,
249
- main_metrics: None | list[str] | tuple[str] = None,
284
+ main_metrics: None | str | list[str] | tuple[str] = None,
250
285
  debug_log: None | bool | LoggingConsole = True,
251
286
  store_sets: bool = False,
252
- brunnermunzel_workaround: None | bool = None,
287
+ edge_cases_workaround: None | bool = None,
253
288
  ) -> CompareStatsResult:
254
289
  """Perform comparison for statistical significance of benchmark results using a chosen
255
290
  statistical method.
@@ -273,29 +308,30 @@ def compareStats(
273
308
  comparisons: "foo|0bar vs 0baz", "foo|0bar vs 1qux" and "foo|0baz vs 1qux". Numbers 0 and 1 in
274
309
  benchmark alternative names references source set of benchmark results (sg0 or sg1).
275
310
 
276
- `main_metrics` is either a list/tuple of strings describing containing main metrics for the
277
- purpose of computing of at_least_one_differs flag, or None (then all metrics are main)
311
+ `main_metrics` is either a string, a list/tuple of strings describing containing main metrics
312
+ for the purpose of computing of at_least_one_differs flag, or None (then all metrics are main).
278
313
 
279
314
  `debug_log` is a bool, but could also be None (==False) or a logger object like LoggingConsole.
280
315
 
281
316
  `store_sets` is a bool, True will make store whole corresponding dataset into a BmCompResult
282
317
  instead of a mean of the set.
283
318
 
284
- `brunnermunzel_workaround`: Formally speaking, Brunner Munzel test method is "underspecified"
319
+ `edge_cases_workaround`: Formally speaking, Brunner Munzel test method is "underspecified"
285
320
  in a sense that by design it doesn't handle a case of non-intersecting sample sets, or sets
286
321
  consisting of the same single element (https://aakinshin.net/posts/brunner-munzel-corner-case/).
287
322
  scipy.stats.brunnermunzel() implementation at least in versions 1.10-1.15.2 directly follows the
288
323
  words of the original paper and doesn't introduce handling for that cases, which causes warnings
289
324
  and nans to appear in results. IMO from an engineering point of view this is more like just a
290
- bug (https://github.com/scipy/scipy/issues/22664). `brunnermunzel_workaround` is a flag to
291
- handle it: value of None handles the bug only for method == 'brunnermunzel', a bool otherwise
292
- controls whether to handle the bug irrespective of the method used (this might give different
293
- pvalues for other tests, but this shouldn't affect inference results unless alpha threshold
294
- value is too wild). It should be noted that for small sample sizes (under 10 elements in any of
325
+ bug (https://github.com/scipy/scipy/issues/22664). The same issue also applies to Student's
326
+ t-test. `edge_cases_workaround` is a flag to handle it: value of None handles the bug only for
327
+ affected methods, a bool otherwise controls whether to handle the bug irrespective of the method
328
+ used (this might give different pvalues for other tests, but this shouldn't affect inference
329
+ results unless alpha threshold value is too wild).
330
+ It should be noted that for small sample sizes (under 10 elements in any of
295
331
  sample sets) this most certainly leads to a wrong p-value result that underestimate a
296
332
  probability of error. However, with 10, or preferably more, sample set sizes, this should be
297
333
  negligible.
298
- While brunnermunzel_workaround allows to disable the workaround, I doubt it has any practical
334
+ While edge_cases_workaround allows to disable the workaround, I doubt it has any practical
299
335
  value in the context of benchmark results comparison.
300
336
 
301
337
  Returns an instance of CompareStatsResult class
@@ -306,7 +342,9 @@ def compareStats(
306
342
  assert isinstance(sg0, dict) and (isinstance(sg1, dict) or (with_alternatives and sg1 is None))
307
343
  assert isinstance(method, str) and method in kMethods, "unsupported method"
308
344
  assert isinstance(alpha, kAllowedFpTypes) and 0 < alpha and alpha < 0.5
309
- assert brunnermunzel_workaround is None or isinstance(brunnermunzel_workaround, bool)
345
+ assert edge_cases_workaround is None or isinstance(edge_cases_workaround, bool)
346
+ if isinstance(main_metrics, str):
347
+ main_metrics = (main_metrics,)
310
348
  assert main_metrics is None or isinstance(main_metrics, (list, tuple))
311
349
 
312
350
  if debug_log is None or (isinstance(debug_log, bool) and not debug_log):
@@ -317,8 +355,8 @@ def compareStats(
317
355
  logger = debug_log
318
356
  debug_log = True
319
357
 
320
- if brunnermunzel_workaround is None:
321
- brunnermunzel_workaround = method == "brunnermunzel"
358
+ if edge_cases_workaround is None:
359
+ edge_cases_workaround = method in ("brunnermunzel", "ttest_ind")
322
360
 
323
361
  def warn(*args, **kwargs):
324
362
  if debug_log:
@@ -330,11 +368,11 @@ def compareStats(
330
368
 
331
369
  def computePValues(s0, s1):
332
370
  """Computes and return pvalues of s0 being stochastically less or greater than s1"""
333
- if brunnermunzel_workaround:
371
+ if edge_cases_workaround:
334
372
  # test for edge cases that brunnermunzel can't handle properly
335
373
  mn0, mx0, mn1, mx1 = np.min(s0), np.max(s0), np.min(s1), np.max(s1)
336
374
  assert np.all(np.isfinite([mn0, mx0, mn1, mx1])) # sanity check, more asserts below
337
- all_eq = np.all([mn0 == mx0, mn0 == mn1, mn0 == mx1])
375
+ all_eq = all([mn0 == mx0, mn0 == mn1, mn0 == mx1])
338
376
  all_less, all_greater = mx0 < mn1, mn0 > mx1
339
377
 
340
378
  if all_eq:
@@ -348,7 +386,7 @@ def compareStats(
348
386
  res_greater = stat_func(s0, s1, alternative="greater")
349
387
 
350
388
  less_pvalue, greater_pvalue = res_less.pvalue, res_greater.pvalue
351
- if brunnermunzel_workaround:
389
+ if edge_cases_workaround:
352
390
  assert np.all(np.isfinite([less_pvalue, greater_pvalue]))
353
391
 
354
392
  return less_pvalue, greater_pvalue
@@ -447,9 +485,10 @@ def compareStats(
447
485
  return bm_results
448
486
 
449
487
  results = {}
488
+ comparisons: dict[str, tuple[str, str]] = {} # {comparison_name -> (bm_name0, bm_name1)}
450
489
 
451
490
  if with_alternatives:
452
- pool = poolBenchmarks(alt_delimiter, sg0, sg1, logger if debug_log else None)
491
+ pool, orig_names = poolBenchmarks(alt_delimiter, sg0, sg1, logger if debug_log else None)
453
492
 
454
493
  for common_name, alternatives in pool.items():
455
494
  if len(alternatives) <= 1:
@@ -460,12 +499,14 @@ def compareStats(
460
499
  )
461
500
  continue
462
501
 
502
+ onames = orig_names[common_name]
463
503
  for altA, altB in itertools.combinations(alternatives.keys(), 2):
464
504
  assert isinstance(altA, str) and isinstance(altB, str)
465
505
  assert altA != altB
466
506
  bm_name = f"{common_name} {alt_delimiter} {altA} vs {altB}"
467
507
  assert bm_name not in results
468
508
  results[bm_name] = compareBenchmark(bm_name, alternatives[altA], alternatives[altB])
509
+ comparisons[bm_name] = (onames[altA], onames[altB])
469
510
 
470
511
  else:
471
512
  common_bms = sg0.keys() & sg1.keys()
@@ -483,5 +524,6 @@ def compareStats(
483
524
  warn("Key/benchmark name '%s' not found in set2", bm_name)
484
525
  continue
485
526
  results[bm_name] = compareBenchmark(bm_name, metrics1, sg1[bm_name])
527
+ comparisons[bm_name] = (bm_name, bm_name)
486
528
 
487
- return CompareStatsResult(results, method, alpha, bool(at_least_one_differs))
529
+ return CompareStatsResult(results, method, alpha, bool(at_least_one_differs), comparisons)
@@ -6,6 +6,7 @@ of Python callables. Some parts, however, could be reused outside of Python benc
6
6
  import argparse
7
7
  from collections import namedtuple
8
8
  from collections.abc import Callable, Iterable
9
+ from copy import deepcopy
9
10
  import itertools
10
11
  import inspect
11
12
  import numpy as np
@@ -66,7 +67,8 @@ class BenchmarkDescription(
66
67
  args_func: Callable[[], Iterable] | None = None,
67
68
  clear_cache_func: Callable[[Iterable], None] | None = None,
68
69
  *,
69
- wait_complete: Callable[[Any], Any] | None = None, # sets the default value for the 2 below
70
+ # wait_complete sets the default value for the other 2 below
71
+ wait_complete: Callable[[Any], Any] | None = None,
70
72
  wait_arg_complete: Callable[[Any], Any] | None = None,
71
73
  wait_func_complete: Callable[[Any], Any] | None = None,
72
74
  ):
@@ -362,7 +364,7 @@ def bench2(func1, func2, **kwargs):
362
364
 
363
365
  def resultsToDict(
364
366
  results: np.ndarray, bm_names: tuple | list | str = "code", alt_delimiter: str | None = None
365
- ) -> tuple[dict[str, np.ndarray], str]:
367
+ ) -> tuple[dict[str, np.ndarray], str, dict[str, str]]:
366
368
  """Store benchmark results in a dictionary benchmark_name -> benchmark_results
367
369
 
368
370
  Arguments:
@@ -385,11 +387,13 @@ def resultsToDict(
385
387
  of functions benchmarked (results.shape[0]), where each string is parsed
386
388
  according to the expected format: "<common_name>{alt_delimiter}<alternative_name>".
387
389
  Benchmarks with the same "<common_name>" are compared pairwise.
388
- Returns a tuple of 2 objects:
390
+ Returns a tuple of 3 objects:
389
391
  dict[str, np.ndarray]: A dictionary mapping benchmark names to benchmark results as 2D
390
392
  matrix of shape (reps, iters). Note that matricies returned are views into the original
391
393
  results array, so modifying them will modify the original results array as well.
392
394
  str - possibly modified alt_delimiter value
395
+ dict[str, int] - mapping of created benchmark names to indices of benchmark data in the
396
+ results array
393
397
  """
394
398
  if results.ndim == 2:
395
399
  results = np.expand_dims(results, axis=0)
@@ -406,13 +410,17 @@ def resultsToDict(
406
410
  n_names = len(bm_names)
407
411
  assert n_names > 0, bm_names_err
408
412
 
409
- def addBenchmark(sg: dict, bm_name: str, b_idx: int):
413
+ sg = {}
414
+ bm_idxs = {}
415
+
416
+ def addBenchmark(bm_name: str, b_idx: int):
417
+ nonlocal sg
410
418
  assert bm_name not in sg, (
411
419
  f"Duplicate benchmark name '{bm_name}' found. Please provide unique names."
412
420
  )
413
421
  sg[bm_name] = results[b_idx, :, :] # .copy()
422
+ bm_idxs[bm_name] = b_idx
414
423
 
415
- sg = {}
416
424
  if alt_delimiter is None:
417
425
  if n_funcs % n_names != 0:
418
426
  raise ValueError(
@@ -423,8 +431,8 @@ def resultsToDict(
423
431
  alt_delimiter = "|"
424
432
 
425
433
  if n_funcs_per_bm <= 1:
426
- addBenchmark(sg, f"{bm_names[0]}{alt_delimiter}0", 0)
427
- addBenchmark(sg, f"{bm_names[0]}{alt_delimiter}same", 0)
434
+ addBenchmark(f"{bm_names[0]}{alt_delimiter}0", 0)
435
+ addBenchmark(f"{bm_names[0]}{alt_delimiter}same", 0)
428
436
  else:
429
437
  combination_set = list(itertools.combinations(range(n_funcs_per_bm), 2))
430
438
  func_group_idx = 0
@@ -432,10 +440,10 @@ def resultsToDict(
432
440
  for cs in combination_set:
433
441
  n1 = f"{bm_name}{alt_delimiter}{cs[0]}"
434
442
  if n1 not in sg:
435
- addBenchmark(sg, n1, func_group_idx + cs[0])
443
+ addBenchmark(n1, func_group_idx + cs[0])
436
444
  n2 = f"{bm_name}{alt_delimiter}{cs[1]}"
437
445
  if n2 not in sg:
438
- addBenchmark(sg, n2, func_group_idx + cs[1])
446
+ addBenchmark(n2, func_group_idx + cs[1])
439
447
  func_group_idx += n_funcs_per_bm
440
448
 
441
449
  else:
@@ -453,9 +461,9 @@ def resultsToDict(
453
461
  raise ValueError(
454
462
  f"Duplicate benchmark name '{bm_name}' found. Please provide unique names."
455
463
  )
456
- addBenchmark(sg, bm_name, b_idx)
464
+ addBenchmark(bm_name, b_idx)
457
465
 
458
- return sg, alt_delimiter
466
+ return sg, alt_delimiter, bm_idxs
459
467
 
460
468
 
461
469
  def _splitCompareStats_and_renderArgs(
@@ -495,24 +503,31 @@ def _splitCompareStats_and_renderArgs(
495
503
 
496
504
 
497
505
  def showBench(
498
- results: np.ndarray,
506
+ results: np.ndarray | dict[str, np.ndarray],
499
507
  *,
500
- bm_names: tuple | list | str = "code",
508
+ bm_names: tuple | list | str | None = "code",
501
509
  alt_delimiter: str | None = None,
502
510
  metrics: dict = {"mean": np.mean, "min": np.min},
503
511
  console: None | LoggingConsole = None,
504
- pvalue_stats_bootstrap: int = 100,
512
+ pvalue_stats_bootstrap: int = 1000,
505
513
  pvalue_stats_bootstrap_seed: Any = None,
514
+ start_with_reshuffled: bool = False,
506
515
  show_progress_each: int = 1,
507
516
  render_report: bool = True,
517
+ same_console_for_progress: bool = False,
518
+ allow_inplace_reshuffle: bool = False,
508
519
  **kwCompareStats_and_renderArgs,
509
520
  ) -> CompareStatsResult:
510
521
  """
511
522
  Displays the benchmark results in a human-readable format.
512
523
 
513
524
  Args:
514
- - results (np.ndarray): The benchmark results as a 3D [nfuncs, reps, iters] or 2D [reps, iters]
515
- numpy array. Essentially, the output of the bench() function.
525
+ - results (np.ndarray | dict[str, np.ndarray]): The benchmark results as a 3D
526
+ [nfuncs, reps, iters] or 2D [reps, iters] numpy array. Essentially, the output of the
527
+ bench() function.
528
+ Alternatively, could be a dictionary mapping benchmark names to benchmark results as 2D
529
+ matrix of potentially different shapes (reps, iters). In that case, `bm_names` must be None
530
+ and `alt_delimiter` must be a valid delimiter string.
516
531
  - bm_names (tuple|list|str, optional) and alt_delimiter: (str|None, optional): defines the
517
532
  names of the benchmarks and how they are compared one against the other. Options are:
518
533
  - if alt_delimiter is None:
@@ -546,12 +561,28 @@ def showBench(
546
561
  assumption of the statistical tests.
547
562
  Note that large values like 1000 are computationally expensive, but provide more stable
548
563
  results thanks to a better coverage.
564
+ By default the first comparison is computed on the original results and the rest
565
+ `pvalue_stats_bootstrap` comparisons are computed on reshuffled results. This can be changed
566
+ by setting `start_with_reshuffled` to True, which make even the first comparison computed on
567
+ reshuffled results, doing `pvalue_stats_bootstrap - 1` iterations of bootstrapping in total.
549
568
  - pvalue_stats_bootstrap_seed (Any, default None): if pvalue bootstrapping is enabled, this seed
550
569
  will initialize the random number generator for bootstrapping.
570
+ - start_with_reshuffled (bool, False by default): if True and bootstrapping is enabled, even the
571
+ first comparison will be computed on reshuffled results. Otherwise it is ignored.
572
+ Note that benchmarking results rendering doesn't currently support judging the comparison
573
+ result by the p-value statistics (it always use results of the first comparison instead), so
574
+ it's not recommended to use this option if your code or rendered results interpretation
575
+ doesn't have an additional logic to take the bootstrapped results into account.
551
576
  - show_progress_each - if p-value bootstrapping is enabled and it's a positive integer,
552
577
  show progress bars and update them each that many bootstrapping iterations.
578
+ - same_console_for_progress (bool, False by default): If True, use the same console for progress
579
+ bar. Useful for using in scripts, but is disabled by default to prevent effect on exported
580
+ reports.
553
581
  - render_report (bool, True by default): If False, only returns CompareStatsResult object,
554
582
  but doesn't print the report.
583
+ - allow_inplace_reshuffle (bool, False by default): If True, allow the `results` to be
584
+ reshuffled for bootstrapping (or if .ndim < 3 for array argument) in place. Otherwise,
585
+ a copy of the results is made when necessary.
555
586
  - kwCompareStats_and_renderArgs: Any optional arguments of the compareStats()
556
587
  or renderComparisonResults() functions. By default compareStats() also gets
557
588
  store_sets=True argument, and renderComparisonResults() gets
@@ -567,18 +598,30 @@ def showBench(
567
598
  kwCompareStats_and_renderArgs["debug_log"] = False
568
599
  assert console is None or isinstance(console, LoggingConsole)
569
600
 
570
- if pvalue_stats_bootstrap > 0 or results.ndim == 2:
571
- results = results.copy()
572
- show_progress = isinstance(show_progress_each, int) and show_progress_each > 0
573
- if results.ndim == 2:
574
- results = np.expand_dims(results, axis=0)
601
+ results_is_dict = isinstance(results, dict)
602
+ if results_is_dict:
603
+ assert bm_names is None and isinstance(alt_delimiter, str) and len(alt_delimiter) > 0
604
+ assert all(isinstance(r, np.ndarray) and r.ndim == 2 for r in results.values())
575
605
 
576
- assert results.ndim == 3, "results must be a 3D numpy array."
606
+ if not allow_inplace_reshuffle and (
607
+ pvalue_stats_bootstrap > 0 or (not results_is_dict and results.ndim < 3)
608
+ ):
609
+ results = deepcopy(results)
610
+ if not results_is_dict and results.ndim < 3:
611
+ results = np.expand_dims(results, axis=0)
612
+ assert results_is_dict or results.ndim == 3, "results must be a dict or 3D numpy array."
613
+
614
+ if pvalue_stats_bootstrap > 0:
615
+ show_progress = isinstance(show_progress_each, int) and show_progress_each > 0
577
616
 
578
617
  compStats_args, render_args = _splitCompareStats_and_renderArgs(
579
618
  kwCompareStats_and_renderArgs, console
580
619
  )
581
- resd, alt_delimiter = resultsToDict(results, bm_names, alt_delimiter)
620
+
621
+ if results_is_dict:
622
+ resd = results
623
+ else:
624
+ resd, alt_delimiter, bm_idxs = resultsToDict(results, bm_names, alt_delimiter)
582
625
 
583
626
  def _applyMetrics(r: dict) -> dict:
584
627
  return {
@@ -586,24 +629,41 @@ def showBench(
586
629
  for bm_name, res in r.items()
587
630
  }
588
631
 
632
+ rng = None
633
+ if pvalue_stats_bootstrap > 0 and start_with_reshuffled:
634
+ pvalue_stats_bootstrap -= 1
635
+ if pvalue_stats_bootstrap <= 0 and console is not None:
636
+ console.warning(
637
+ "pvalue_stats_bootstrap is 0, so no bootstrapping will be done even though start_with_reshuffled is True."
638
+ )
639
+
640
+ rng = np.random.default_rng(pvalue_stats_bootstrap_seed)
641
+ for res in resd.values():
642
+ rng.shuffle(res.reshape(-1))
643
+
589
644
  sg = _applyMetrics(resd)
590
645
  sr = compareStats(sg, None, alt_delimiter=alt_delimiter, **compStats_args)
591
646
 
592
647
  if pvalue_stats_bootstrap > 0:
593
- rng = np.random.default_rng(pvalue_stats_bootstrap_seed)
594
- reps, iters = results.shape[1:]
648
+ if rng is None:
649
+ rng = np.random.default_rng(pvalue_stats_bootstrap_seed)
595
650
 
596
651
  if show_progress:
597
- progress = Progress(transient=True)
652
+ progress = Progress(
653
+ transient=True, console=console if same_console_for_progress else None
654
+ )
598
655
  task = progress.add_task("P-value bootstrapping", total=pvalue_stats_bootstrap)
599
656
  progress.start()
600
657
 
658
+ bs_compStats_args = compStats_args.copy()
659
+ bs_compStats_args["store_sets"] = False
660
+ bs_compStats_args["debug_log"] = False
661
+
601
662
  for i in range(pvalue_stats_bootstrap):
602
- for _, res in resd.items():
663
+ for res in resd.values():
603
664
  rng.shuffle(res.reshape(-1))
604
- assert res.shape == (reps, iters)
605
665
  sg = _applyMetrics(resd)
606
- cs = compareStats(sg, None, alt_delimiter=alt_delimiter, **compStats_args)
666
+ cs = compareStats(sg, None, alt_delimiter=alt_delimiter, **bs_compStats_args)
607
667
  sr.updatePvalStats(cs)
608
668
 
609
669
  if show_progress and i % show_progress_each == 0:
@@ -612,6 +672,11 @@ def showBench(
612
672
  if show_progress:
613
673
  progress.stop()
614
674
 
675
+ if not results_is_dict:
676
+ sr.setComparisonIndices({
677
+ bm_name: (bm_idxs[bm0], bm_idxs[bm1]) for bm_name, (bm0, bm1) in sr.comparisons.items()
678
+ })
679
+
615
680
  if render_report:
616
681
  renderComparisonResults(sr, console=console, **render_args)
617
682
  return sr
@@ -731,32 +796,32 @@ def makeArgumentParser(parser=None, *, allow_exports=True):
731
796
  "--iters",
732
797
  type=int,
733
798
  default=_g_bench_defaults["iters"],
734
- help=f"Iterations per repetition (default: {_g_bench_defaults['iters']})",
799
+ help="Iterations per repetition (default: %(default)s)",
735
800
  )
736
801
  parser.add_argument(
737
802
  "--reps",
738
803
  type=int,
739
804
  default=_g_bench_defaults["reps"],
740
- help=f"Number of repetitions (default: {_g_bench_defaults['reps']})",
805
+ help="Number of repetitions (default: %(default)s)",
741
806
  )
742
807
  parser.add_argument(
743
808
  "--warmup",
744
809
  type=int,
745
810
  default=_g_bench_defaults["warmup"],
746
- help=f"Number of warmup iterations (default: {_g_bench_defaults['warmup']})",
811
+ help="Number of warmup iterations (default: %(default)s)",
747
812
  )
748
813
  parser.add_argument(
749
814
  "--batch_functions",
750
815
  action=argparse.BooleanOptionalAction,
751
816
  default=_g_bench_defaults["batch_functions"],
752
817
  help="If true: outer loop - benchmarks, inner loop - iterations. Otherwise reversed: "
753
- f"outer loop - iterations, inner loop - benchmarks. (default: {_g_bench_defaults['batch_functions']})",
818
+ "outer loop - iterations, inner loop - benchmarks. (default: %(default)s)",
754
819
  )
755
820
  parser.add_argument(
756
821
  "--randomize_iterations",
757
822
  action=argparse.BooleanOptionalAction,
758
823
  default=_g_bench_defaults["randomize_iterations"],
759
- help=f"Randomly shuffle benchmarks order (default: {_g_bench_defaults['randomize_iterations']})",
824
+ help="Randomly shuffle benchmarks order (default: %(default)s)",
760
825
  )
761
826
 
762
827
  parser.add_argument(
@@ -764,7 +829,15 @@ def makeArgumentParser(parser=None, *, allow_exports=True):
764
829
  type=int,
765
830
  default=_g_showBench_defaults["pvalue_stats_bootstrap"],
766
831
  help="Number of iterations for pvalue statistics bootstrapping (default: "
767
- f"{_g_showBench_defaults['pvalue_stats_bootstrap']}). Set to 0 to disable.",
832
+ "%(default)s). Set to 0 to disable.",
833
+ )
834
+
835
+ parser.add_argument(
836
+ "--start_with_reshuffled",
837
+ action=argparse.BooleanOptionalAction,
838
+ default=_g_showBench_defaults["start_with_reshuffled"],
839
+ help="If true, start even the first pvalue statistics bootstrapping iteration with "
840
+ "reshuffled results (default: %(default)s).",
768
841
  )
769
842
 
770
843
  if allow_exports:
@@ -132,7 +132,7 @@ def renderComparisonResults(
132
132
  dark_theme: bool = True,
133
133
  title: None | bool | str = True, # None, False - disables title, str - customizes it
134
134
  style_overrides: dict = None, # overrides for kDefaultStyles
135
- main_metrics: Iterable[str] = None,
135
+ main_metrics: str | Iterable[str] | None = None,
136
136
  show_sample_sizes: bool = False,
137
137
  sample_stats=None, # or iterable with predefined values: float%, or from kPossibleStatNames.keys()
138
138
  expect_same: bool = False, # if true, show stats from assumption h0 is true
@@ -183,17 +183,6 @@ def renderComparisonResults(
183
183
  )
184
184
  assert title is None or isinstance(title, str)
185
185
 
186
- if show_pvalue_stats:
187
- if not comp_res.pval_stats_available:
188
- show_pvalue_stats = False
189
- console.debug("P-value statistics are not available, disabling show_pvalue_stats")
190
- else:
191
- pvs_iters = None
192
- pvs_styles = [ # indexed by is_main
193
- [_getFmt(f"metric_{fld}_diff" if i < 2 else f"metric_{fld}_same") for i in range(3)]
194
- for fld in ("scnd", "main")
195
- ]
196
-
197
186
  delim_space = "\n" if multiline else " "
198
187
 
199
188
  _column_descr = (
@@ -203,6 +192,8 @@ def renderComparisonResults(
203
192
  )
204
193
 
205
194
  metrics = comp_res.getMetrics()
195
+ if isinstance(main_metrics, str):
196
+ main_metrics = (main_metrics,)
206
197
  if main_metrics is None or len(main_metrics) < 1:
207
198
  main_metrics = [metrics[0]]
208
199
  else:
@@ -211,6 +202,17 @@ def renderComparisonResults(
211
202
  scnd_metrics = [m for m in metrics if m not in main_metrics]
212
203
  iter_metrics = [*main_metrics, *scnd_metrics]
213
204
 
205
+ if show_pvalue_stats:
206
+ if not comp_res.pval_stats_available:
207
+ show_pvalue_stats = False
208
+ console.debug("P-value statistics are not available, disabling show_pvalue_stats")
209
+ else:
210
+ pvs_iters = None
211
+ pvs_styles = [ # indexed by is_main
212
+ [_getFmt(f"metric_{fld}_diff" if i < 2 else f"metric_{fld}_same") for i in range(3)]
213
+ for fld in ("scnd", "main")
214
+ ]
215
+
214
216
  metric_unit_keys = [f"metric_{m}_unit" for m in iter_metrics]
215
217
 
216
218
  assert isinstance(metric_precision, int)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: benchstats
3
- Version: 3.3.2
3
+ Version: 3.4.0
4
4
  Summary: Statistical Testing for Benchmark Results Comparison
5
5
  Author-email: Aleksei Rechinskii <5dpea9nhd@mozmail.com>
6
6
  License-Expression: MIT
@@ -9,7 +9,7 @@ kAlpha = bsc.kDefaultAlpha
9
9
 
10
10
  class TestCompareStatsResult(unittest.TestCase):
11
11
  def test_updatePvalStats(self):
12
- defargs = (kMethods[0], kAlpha, True)
12
+ defargs = (kMethods[0], kAlpha, True,{})
13
13
  cs0 = bsc.CompareStatsResult(
14
14
  {
15
15
  "b1": {
@@ -109,4 +109,7 @@ class TestComparisonMethods(unittest.TestCase):
109
109
 
110
110
 
111
111
  if __name__ == "__main__":
112
- unittest.main()
112
+ import sys
113
+ import pytest
114
+
115
+ sys.exit(pytest.main(sys.argv))
@@ -1,8 +1,6 @@
1
- import sys
2
1
  from typing import Iterable
3
2
  import unittest
4
3
  import numpy as np
5
- import pytest
6
4
  import time
7
5
  import traceback
8
6
 
@@ -232,7 +230,7 @@ class TestQBench(unittest.TestCase):
232
230
  check_reset_counters(7 * 2, 7 * 2, 7 * 2, 0, 7 + 14)
233
231
 
234
232
  def test_showBench_with_delimiter(self):
235
- def_prms = {"alt_delimiter": "|", "render_report": False}
233
+ def_prms = {"alt_delimiter": "|", "render_report": False, "show_progress_each": 0}
236
234
  nreps = 10
237
235
  results = np.stack(
238
236
  (
@@ -246,13 +244,24 @@ class TestQBench(unittest.TestCase):
246
244
  axis=0,
247
245
  )
248
246
  bm_names = ("alg0|A", "alg0|B", "alg1|A", "alg1|B", "alg2|A", "alg2|B")
249
- sr = qb.showBench(results, bm_names=bm_names, **def_prms)
250
- assert "<" == sr.results["alg0 | A vs B"]["mean"].result
251
- assert ">" == sr.results["alg1 | A vs B"]["mean"].result
252
- assert "~" == sr.results["alg2 | A vs B"]["mean"].result
247
+ cr = qb.showBench(results, bm_names=bm_names, **def_prms)
248
+ assert "<" == cr.results["alg0 | A vs B"]["mean"].result
249
+ assert ">" == cr.results["alg1 | A vs B"]["mean"].result
250
+ assert "~" == cr.results["alg2 | A vs B"]["mean"].result
251
+ assert cr.at_least_one_differs
252
+ assert cr.comparisons == {
253
+ "alg0 | A vs B": ("alg0|A", "alg0|B"),
254
+ "alg1 | A vs B": ("alg1|A", "alg1|B"),
255
+ "alg2 | A vs B": ("alg2|A", "alg2|B"),
256
+ }
257
+ assert cr.comparison_indices == {
258
+ "alg0 | A vs B": (0, 1),
259
+ "alg1 | A vs B": (2, 3),
260
+ "alg2 | A vs B": (4, 5),
261
+ }
253
262
 
254
263
  def test_showBench_one_name(self):
255
- def_prms = {"render_report": False}
264
+ def_prms = {"render_report": False, "show_progress_each": 0}
256
265
  nreps = 10
257
266
  results = np.stack(
258
267
  (
@@ -262,13 +271,24 @@ class TestQBench(unittest.TestCase):
262
271
  ),
263
272
  axis=0,
264
273
  )
265
- sr = qb.showBench(results, bm_names="code", **def_prms)
266
- assert "<" == sr.results["code | 0 vs 1"]["mean"].result
267
- assert ">" == sr.results["code | 1 vs 2"]["mean"].result
268
- assert ">" == sr.results["code | 0 vs 2"]["mean"].result
274
+ cr = qb.showBench(results, bm_names="code", **def_prms)
275
+ assert "<" == cr.results["code | 0 vs 1"]["mean"].result
276
+ assert ">" == cr.results["code | 1 vs 2"]["mean"].result
277
+ assert ">" == cr.results["code | 0 vs 2"]["mean"].result
278
+ assert cr.at_least_one_differs
279
+ assert cr.comparisons == {
280
+ "code | 0 vs 1": ("code|0", "code|1"),
281
+ "code | 1 vs 2": ("code|1", "code|2"),
282
+ "code | 0 vs 2": ("code|0", "code|2"),
283
+ }
284
+ assert cr.comparison_indices == {
285
+ "code | 0 vs 1": (0, 1),
286
+ "code | 1 vs 2": (1, 2),
287
+ "code | 0 vs 2": (0, 2),
288
+ }
269
289
 
270
290
  def test_showBench_tuple(self):
271
- def_prms = {"render_report": False}
291
+ def_prms = {"render_report": False, "show_progress_each": 0}
272
292
  nreps = 10
273
293
  results = np.stack(
274
294
  (
@@ -282,10 +302,21 @@ class TestQBench(unittest.TestCase):
282
302
  axis=0,
283
303
  )
284
304
  bm_names = ("alg0", "alg1", "alg2")
285
- sr = qb.showBench(results, bm_names=bm_names, **def_prms)
286
- assert "<" == sr.results["alg0 | 0 vs 1"]["mean"].result
287
- assert ">" == sr.results["alg1 | 0 vs 1"]["mean"].result
288
- assert "~" == sr.results["alg2 | 0 vs 1"]["mean"].result
305
+ cr = qb.showBench(results, bm_names=bm_names, **def_prms)
306
+ assert "<" == cr.results["alg0 | 0 vs 1"]["mean"].result
307
+ assert ">" == cr.results["alg1 | 0 vs 1"]["mean"].result
308
+ assert "~" == cr.results["alg2 | 0 vs 1"]["mean"].result
309
+ assert cr.at_least_one_differs
310
+ assert cr.comparisons == {
311
+ "alg0 | 0 vs 1": ("alg0|0", "alg0|1"),
312
+ "alg1 | 0 vs 1": ("alg1|0", "alg1|1"),
313
+ "alg2 | 0 vs 1": ("alg2|0", "alg2|1"),
314
+ }
315
+ assert cr.comparison_indices == {
316
+ "alg0 | 0 vs 1": (0, 1),
317
+ "alg1 | 0 vs 1": (2, 3),
318
+ "alg2 | 0 vs 1": (4, 5),
319
+ }
289
320
 
290
321
  def test_showBench_pvalue_stats_bootstrap(self):
291
322
  r = np.array([[[1, 3, 3], [1, 3, 3]], [[1, 2, 2], [1, 2, 2]]])
@@ -295,6 +326,8 @@ class TestQBench(unittest.TestCase):
295
326
  alpha=0.499,
296
327
  render_report=False,
297
328
  console=None,
329
+ show_progress_each=0,
330
+ start_with_reshuffled=False,
298
331
  )
299
332
  # seed is chosen to modify the first bootstrap result and produce the same second result
300
333
 
@@ -306,12 +339,20 @@ class TestQBench(unittest.TestCase):
306
339
  assert len(pvs["~"]) == same_len
307
340
  assert all(pvs["~"][i] == 1.0 for i in range(same_len))
308
341
 
342
+ cr = qb.showBench(r, pvalue_stats_bootstrap=0, **cmnargs)
343
+ assert not cr.at_least_one_differs
344
+
309
345
  cr = qb.showBench(r, pvalue_stats_bootstrap=1, **cmnargs)
346
+ assert cr.at_least_one_differs
310
347
  test_pvs(next(iter(next(iter(cr.pval_stats.values())).values())), 1)
311
348
 
312
349
  cr = qb.showBench(r, pvalue_stats_bootstrap=2, **cmnargs)
350
+ assert cr.at_least_one_differs
313
351
  test_pvs(next(iter(next(iter(cr.pval_stats.values())).values())), 2)
314
352
 
315
353
 
316
354
  if __name__ == "__main__":
355
+ import sys
356
+ import pytest
357
+
317
358
  sys.exit(pytest.main(sys.argv))
File without changes
File without changes
File without changes
File without changes