nscore 0.1.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 (41) hide show
  1. nscore-0.1.0/PKG-INFO +16 -0
  2. nscore-0.1.0/README.md +144 -0
  3. nscore-0.1.0/nscore/__init__.py +0 -0
  4. nscore-0.1.0/nscore/batch.py +149 -0
  5. nscore-0.1.0/nscore/nonparametric_nsm.py +577 -0
  6. nscore-0.1.0/nscore/nsm.py +703 -0
  7. nscore-0.1.0/nscore/savi.py +626 -0
  8. nscore-0.1.0/nscore/tools/__init__.py +0 -0
  9. nscore-0.1.0/nscore/tools/plotting.py +239 -0
  10. nscore-0.1.0/nscore/utils/__init__.py +2 -0
  11. nscore-0.1.0/nscore/utils/utils_general.py +323 -0
  12. nscore-0.1.0/nscore/utils/utils_wsr.py +151 -0
  13. nscore-0.1.0/nscore/wsr.py +159 -0
  14. nscore-0.1.0/nscore.egg-info/PKG-INFO +16 -0
  15. nscore-0.1.0/nscore.egg-info/SOURCES.txt +39 -0
  16. nscore-0.1.0/nscore.egg-info/dependency_links.txt +1 -0
  17. nscore-0.1.0/nscore.egg-info/requires.txt +6 -0
  18. nscore-0.1.0/nscore.egg-info/top_level.txt +2 -0
  19. nscore-0.1.0/scripts/__init__.py +0 -0
  20. nscore-0.1.0/scripts/general/__init__.py +0 -0
  21. nscore-0.1.0/scripts/general/generate_cld_plot.py +133 -0
  22. nscore-0.1.0/scripts/general/large_scale_bernoulli_test_gather_data.py +445 -0
  23. nscore-0.1.0/scripts/general/large_scale_bernoulli_test_process_data.py +106 -0
  24. nscore-0.1.0/scripts/general/large_scale_bernoulli_test_visualize_data.py +542 -0
  25. nscore-0.1.0/scripts/general/multivariate_savi_gather_data.py +113 -0
  26. nscore-0.1.0/scripts/general/multivariate_savi_process_data.py +30 -0
  27. nscore-0.1.0/scripts/general/nonparametric_density_evaluation_summary_statistics.py +48 -0
  28. nscore-0.1.0/scripts/general/nonparametric_density_evaluations.py +181 -0
  29. nscore-0.1.0/scripts/paper_results/__init__.py +0 -0
  30. nscore-0.1.0/scripts/paper_results/evaluate_full_RL_results.py +219 -0
  31. nscore-0.1.0/scripts/paper_results/lbm_data_eval_combine_part1.py +49 -0
  32. nscore-0.1.0/scripts/paper_results/lbm_data_eval_combine_part2.py +48 -0
  33. nscore-0.1.0/scripts/paper_results/lbm_data_eval_combine_part3.py +49 -0
  34. nscore-0.1.0/scripts/paper_results/lbm_data_eval_part1.py +281 -0
  35. nscore-0.1.0/scripts/paper_results/lbm_data_eval_part2.py +268 -0
  36. nscore-0.1.0/scripts/paper_results/lbm_data_eval_part3.py +267 -0
  37. nscore-0.1.0/scripts/paper_results/process_full_RL_results.py +58 -0
  38. nscore-0.1.0/scripts/paper_results/rl_mujoco_cartpole_data.py +27 -0
  39. nscore-0.1.0/setup.cfg +4 -0
  40. nscore-0.1.0/setup.py +18 -0
  41. nscore-0.1.0/tests/test_nscore_deprecation_shims.py +89 -0
nscore-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,16 @@
1
+ Metadata-Version: 2.4
2
+ Name: nscore
3
+ Version: 0.1.0
4
+ Summary: Sequential statistical hypothesis testing for generalized performance measures.
5
+ Author: David Snyder, Haruki Nishimura
6
+ Author-email: dsnyder5@engineering.upenn.edu, haruki.nishimura@tri.global
7
+ Requires-Dist: matplotlib
8
+ Requires-Dist: numpy>=1.20
9
+ Requires-Dist: scipy
10
+ Requires-Dist: cvxpy
11
+ Requires-Dist: statistical-comparison-core<0.3,>=0.2.0
12
+ Requires-Dist: statistical-comparison-helpers
13
+ Dynamic: author
14
+ Dynamic: author-email
15
+ Dynamic: requires-dist
16
+ Dynamic: summary
nscore-0.1.0/README.md ADDED
@@ -0,0 +1,144 @@
1
+ # README
2
+ Instructions for installation and usage of NSCORE, a nonparametric sequential procedure for rigorous robot policy comparison. NSCORE has minimal and lightweight computational requirements, and is easy to wrap around existing simulation and hardware evaluation pipelines.
3
+
4
+ ## Installation
5
+
6
+ Install NSCORE from the repository root with pip:
7
+
8
+ ```bash
9
+ pip install .
10
+ ```
11
+
12
+ For local development, use an editable install:
13
+
14
+ ```bash
15
+ pip install -e .
16
+ ```
17
+
18
+ NSCORE depends on `statistical-comparison-core` and
19
+ `statistical-comparison-helpers`. Until those packages are available from your
20
+ configured package index, install them from their local repositories or ensure
21
+ they are otherwise available on your Python path before installing NSCORE.
22
+
23
+ ### Optional conda environment
24
+
25
+ Conda is not required to install NSCORE, but it can be used to create an isolated
26
+ environment:
27
+
28
+ ```bash
29
+ conda create -n nscore --file requirements_conda.txt
30
+ conda activate nscore
31
+ pip install .
32
+ ```
33
+
34
+ ## Applicability (Non-Technical)
35
+ For additional motivation of N-SCORE and related methods, see Tutorials, below.
36
+
37
+ ### Understanding the Arguments: Hypotheses
38
+ NSCORE performs _mean comparison_ between two different random variables of unknown (possibly nonparametric) distribution shape. We will term the variables $R_0$ and $R_1$, with respective (unknown) means $\mu_0$ and $\mu_1$.
39
+
40
+ The first thing that we will need is a _hypothesis_: what do we want the relationship to be between $\mu_0$ and $\mu_1$? For example, if $R_0$ is a measure of a baseline policy's performance and $R_1$ is a measure of the performance of our novel method, we want $\mu_0 < \mu_1$ (under the convention that $R_i$ is a reward, i.e., higher is better). This is specified precisely as `alternative=Hypothesis.P0LessThanP1` (the notation $p$ is derived from the original use case of comparing Bernoulli random variables, where the mean $\mu$ is equivalent to the conventional Bernoulli parameter $p$). Conversely, If the $R_i$ are cost measures, where lower is better, we may wish to specify the alternative $\mu_0 > \mu_1$. This is expressed directly as `alternative=Hypothesis.P0MoreThanP1`.
41
+
42
+ A second use also arises in the form of error checking. Returning to _reward measures_: imagine there might be a bug in our codebase. We might value a method that runs both tests simultaneously. The first test checks if we improve upon the baseline (assuming no bugs), while the second test quickly tells us if there might be a bug making us perform significantly worse than the baseline, allowing us to stop early and fix the problem! This corresponds to the case of a `Mirrored` (Two-Sided) Test, where we keep track of both directions. We will get into Mirrored tests a bit more, later.
43
+
44
+ ### Understanding the Arguments: Alpha
45
+ When we compare ourselves to a baseline method, we want to establish that our performance is better. But evaluations have significant randomness, meaning that we cannot simply use empirical success to argue for policy improvement. Instead, we propose the following evidential justification: "the probability that our robot policy's performance is _not_ better than the baseline policy is less than $\alpha$."
46
+
47
+ This guarantee means that, if our policy was _no better than the baseline_, then the probability of observing as strong empirical evidence in favor of our method is less than $\alpha$. Conversely, we can claim $1-\alpha$ __confidence__ that our new policy is indeed better than the baseline (on the shared distribution of environments that both policies are evaluated on). This is a strong form of generalization that allows us to tune our confidence level to any desired $\alpha \in (0, 1)$.
48
+
49
+ ### Understanding the Arguments: c
50
+ At present, NSCORE takes advantage of certain efficient properties of linear representations of the data-generating distributions. The vector $c \in [0, 1]^K$ corresponds to this representation, which is perhaps best understood as a discretization of the interval $[0, 1]$ into bins (where $c$ encodes the bin edge positions). We then construct an approximation of the distribution law from the empirical counts within each bin.
51
+
52
+ ### Instantiating and Running an NSCORE Test on Full Datasets (Basic Usage)
53
+ To use NSCORE, it is necessary to specify the three aforementioned parameters.
54
+
55
+ ```python
56
+
57
+ alternative: [Hypothesis]
58
+ alpha: [float]
59
+ c: [np.ndarray]
60
+
61
+ ```
62
+
63
+ As an example: for Bernoulli data, to test if our policy $\pi_1$ has a higher success rate than a baseline $\pi_0$ at $95$\% confidence, we would specify the test:
64
+
65
+ ```python
66
+ nscore_test = BernoulliNsmTest(alternative=Hypothesis.P0LessThanP1, alpha=0.05, c=np.arange(2)/1.)
67
+ ```
68
+ and, given an evaluation sequence of success/failure outcomes for each policy, we would run the test as:
69
+ ```python
70
+ nscore_result = nscore_test.run_on_sequence(outcomes_for_pi_0, outcomes_for_pi_1)
71
+ ```
72
+
73
+ ### Instantiating and Running an NSCORE Test on Full Datasets (General Metrics)
74
+ If we have performance measures that are not bounded in $[0, 1]$, then the measures must be __normalized__ using _a priori_ knowledge of the test domain. For example, on the Mujoco InvertedPendulum-v4 task, the reward is bounded by the time horizon $T$; therefore, normalization can be undertaken simply by scaling down the results. First, we define a more detailed NSM test for the more complex performance measure:
75
+ ```python
76
+ nscore_inverted_pendulum_test = ContinuousNsmTest(alternative=Hypothesis.P0LessThanP1, alpha=0.05, c=np.arange(101)/100.)
77
+ ```
78
+ Next, we use the same `run_on_sequence()` functionality, but normalize the scores:
79
+ ```python
80
+ nscore_inverted_pendulum_result = nscore_inverted_pendulum_test.run_on_sequence(outcomes_for_pi_0/T, outcomes_for_pi_1/T)
81
+ ```
82
+
83
+ ### Instantiating and Running an NSCORE Test _Online_ (General Metrics)
84
+ NSCORE can be used to save time by allowing the evaluator to update the evaluation _online_ as they gather trials. The test stops precisely
85
+ when enough evidence has accumulated to be $1-\alpha$ confident that the intended decision is correct. Taking the InvertedPendulum-v4 task as the running example, we first initialize the test as before:
86
+ ```python
87
+ nscore_online_evaluation_test = ContinuousNsmTest(alternative=Hypothesis.P0LessThanP1, alpha=0.05, c=np.arange(101)/100.)
88
+ ```
89
+ and then implement a simple evaluation protocol
90
+ ```python
91
+ time_of_decision = 0
92
+ decided = False
93
+ maximum_number_of_evals_per_policy = 100 # Optional
94
+ while (decided is False) and (time_of_decision < maximum_number_of_evals_per_policy):
95
+ new_datum_pi_0 = run_pi_0_on_new_iid_environment(ics, ...)
96
+ new_datum_pi_1 = run_pi_1_on_new_iid_environment(ics, ...)
97
+
98
+ result = nscore_online_evaluation_test.step(new_datum_pi_0, new_datum_pi_1)
99
+
100
+ if result.decision is Decision.AcceptAlternative:
101
+ decided = True
102
+
103
+ time_of_decision += 1
104
+
105
+ # Print the results
106
+ print(f"NSCORE test decided in {time_of_decision} trials per policy")
107
+ print(f"NSCORE decision: {result.decision}")
108
+ ```
109
+
110
+ ### Understanding an NSCORE Test Result
111
+ We utilize the same structure for test decisions as our previous work, [STEP](https://github.com/TRI-ML/sequentialized_barnard_tests). Formally, Neyman-Pearson statistical testing only allows for _accepting_ the alternative hypothesis (if sufficient evidence for it is accumulated, of course). This is encoded by a `Decision` object; for a realized test that rejects the null and accepts the alternative, the associated decision would be `test.decision = Decision.AcceptAlternative`.
112
+
113
+ Of course, the data might be insufficiently indicative of the alternative. Because the tests are sequential, there is the opportunity to gather more, so we define a placeholder, denoted by `Decision.FailToDecide`. For anytime-valid tests, data may in principle be collected in perpetuity; thus, `Decision.FailToDecide` implies simply that 'not enough information has yet accumulated to make up our mind one way or the other.'
114
+
115
+ Finally, as mentioned earlier, there are practical instances where we might wish to monitor both directions of a comparison. Informally, the standard direction seeks certify that our policy has improved over the baseline, while the other direction gives us reliable statistical evidence that we should stop early and 'give up;' that is, it tells us that our novel method is performing significantly worse, and that gathering more trials is unlikely to change that assessment. This can be used as a bug-catcher, as well as to reliable help with design iterations _when used responsibly_.
116
+
117
+ __We emphasize that a "kitchen sink" approach of just trying a bunch of different $\pi_1^{[i]}$ until a significant result is found against $\pi_0$ constitutes p-hacking, and invalidates statistical assurances__. More precisely, each $\pi_1^{[i]}$ must be accompanied by an additional union bound correction of $\alpha$ -- concretely: if you test five new policies against $\pi_0$, with each test at level $\alpha$, any significant difference found can only be reported at level $5\alpha$! This is the union bound, or "Bonferroni correction" in classical hypothesis testing.
118
+
119
+ However, when used responsibly, the two-sided test can add significant further empirical savings to the practitioner's evaluation burden. In our setup, a two-sided test is termed `Mirrored`, because it will consist of two _mirrored_ one-sided tests; the tests will individually check the hypotheses `Hypothesis.P0LessThanP1` and `Hypothesis.P0MoreThanP1`. In this context, the user will still specify a specific alternative, which then amounts to selecting which hypothesis is the semantic "Alternative" and which is the "Null." From this, we construct a third decision option, `Decision.AcceptNull`, which amounts to accepting the semantic "Null" as the alternative of a second one-sided test. For example:
120
+ ```python
121
+ mirrored_nscore_test = MirroredContinuousNsmTest(alternative=Hypothesis.P0LessThanP1, alpha=0.05, c=np.arange(101)/100.)
122
+ ```
123
+ will induce two one-sided tests:
124
+ ```python
125
+ nscore_test_for_alternative: ContinuousNsmTest(alternative=Hypothesis.P0LessThanP1, alpha=0.05, c=np.arange(101)/100.)
126
+ nscore_test_for_null: ContinuousNsmTest(alternative=Hypothesis.P0MoreThanP1, alpha=0.05, c=np.arange(101)/100.)
127
+ ```
128
+ Formally `result.decision = Decision.AcceptNull` here is equivalent to accepting the _alternative hypothesis_ of `nscore_test_for_null` (as required by Neyman-Pearson testing -- one is not generally allowed to accept a null hypothesis). Semantically, however, we have _informally_ accepted the null in the sense that we have concluded, with high confidence, that $\mu_0 > \mu_1$.
129
+
130
+ ## Tutorials
131
+ Tutorials that illustrate standard use cases of NSCORE can be found as Jupyter Notebooks under `/notebooks`.
132
+
133
+ ## Citation
134
+ If you find this code useful, please cite our work:
135
+
136
+
137
+ ```bibtex
138
+ @inproceedings{snyder_beyond_2026,
139
+ title = {Beyond {Binary} {Success}: {Sample}-{Efficient} and {Statistically} {Rigorous} {Robot} {Policy} {Comparison}},
140
+ author = {Snyder, David and Badithela, Apurva and Matni, Nikolai and Pappas, George and Majumdar, Anirudha and Itkina, Masha and Nishimura, Haruki},
141
+ booktitle={arXiv preprint arXiv:2603.13616}
142
+ year = {2026},
143
+ }
144
+ ```
File without changes
@@ -0,0 +1,149 @@
1
+ """Batch tests.
2
+
3
+ This module defines batch methods for hypothesis testing.
4
+ """
5
+
6
+ import numpy as np
7
+ from numpy.typing import ArrayLike
8
+ from scipy.stats import barnard_exact
9
+
10
+ from statistical_comparison_core import (
11
+ Decision,
12
+ Hypothesis,
13
+ MirroredTestMixin,
14
+ TestBase,
15
+ TestResult,
16
+ )
17
+
18
+
19
+ class BarnardExactTest(TestBase):
20
+ """Barnard's exact test.
21
+
22
+ This class is a wrapper around scipy's implementation of Barnard's exact test.
23
+ For more details, refer to scipy's documentation:
24
+ https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.barnard_exact.html
25
+
26
+ Attributes:
27
+ alternative: Specification of the alternative hypothesis.
28
+ alpha: Significance level of the test.
29
+ """
30
+
31
+ def __init__(self, alternative: Hypothesis, alpha: float) -> None:
32
+ """Initializes the test object.
33
+
34
+ Args:
35
+ alternative: Specification of the alternative hypothesis.
36
+ alpha: Significance level of the test.
37
+ """
38
+ self.alternative = alternative
39
+ self.alpha = alpha
40
+
41
+ def run_on_sequence(
42
+ self, sequence_0: ArrayLike, sequence_1: ArrayLike
43
+ ) -> TestResult:
44
+ """Runs the test on a pair of two Bernoulli sequences.
45
+
46
+ Args:
47
+ sequence_0: Sequence of Bernoulli data from the first source.
48
+ sequence_1: Sequence of Bernoulli data from the second source.
49
+
50
+ Returns:
51
+ TestResult: Result of the hypothesis test.
52
+
53
+ Raises:
54
+ ValueError: If the input sequences are not Bernoulli data.
55
+ """
56
+ sequence_0_is_binary = np.all(
57
+ (np.array(sequence_0) == 0) + (np.array(sequence_0) == 1)
58
+ )
59
+ sequence_1_is_binary = np.all(
60
+ (np.array(sequence_1) == 0) + (np.array(sequence_1) == 1)
61
+ )
62
+ if not (sequence_0_is_binary and sequence_1_is_binary):
63
+ raise (ValueError("Input sequences must be all Bernoulli data."))
64
+ num_successes_0 = np.sum(sequence_0).item()
65
+ num_failures_0 = len(sequence_0) - num_successes_0
66
+ num_successes_1 = np.sum(sequence_1).item()
67
+ num_failures_1 = len(sequence_1) - num_successes_1
68
+ table = [[num_successes_0, num_successes_1], [num_failures_0, num_failures_1]]
69
+
70
+ barnard = barnard_exact(
71
+ table,
72
+ alternative=(
73
+ "less" if self.alternative == Hypothesis.P0LessThanP1 else "greater"
74
+ ),
75
+ pooled=(len(sequence_0) == len(sequence_1)),
76
+ )
77
+ if barnard.pvalue <= self.alpha:
78
+ decision = Decision.AcceptAlternative
79
+ else:
80
+ decision = Decision.FailToDecide
81
+ result = TestResult(
82
+ decision,
83
+ {"p_value": barnard.pvalue.item(), "statistic": barnard.statistic.item()},
84
+ )
85
+ return result
86
+
87
+
88
+ class MirroredBarnardExactTest(MirroredTestMixin, TestBase):
89
+ """A pair of one-sided Barnard's exact tests with mirrored alternatives.
90
+
91
+ In our terminology, a mirrored test is one that runs two one-sided tests
92
+ simultaneously, with the null and the alternaive flipped from each other. This is so
93
+ that it can yield either Decision.AcceptNull or Decision.AcceptAlternative depending
94
+ on the input data, unlike standard one-sided tests that can never 'accept' the null.
95
+ (Those standard tests will at most fail to reject the null, as represented by
96
+ Decision.FailToDecide.)
97
+
98
+ For example, if the alternative is Hypothesis.P0MoreThanP1 and the decision is
99
+ Decision.AcceptNull, it should be interpreted as accepting Hypothesis.P0LessThanP1.
100
+
101
+ The significance level alpha controls the following two errors simultaneously: (1)
102
+ probability of wrongly accepting the alternative when the null is true, and (2)
103
+ probability of wrongly accepting the null when the alternative is true. Note that
104
+ Bonferroni correction is not needed since the null hypothesis for one test is the
105
+ alternative for the other.
106
+
107
+ Attributes:
108
+ alternative: Specification of the alternative hypothesis.
109
+ alpha: Significance level of the test.
110
+ """
111
+
112
+ _base_class = BarnardExactTest
113
+
114
+ def run_on_sequence(
115
+ self, sequence_0: ArrayLike, sequence_1: ArrayLike
116
+ ) -> TestResult:
117
+ """Runs the test on a pair of two Bernoulli sequences.
118
+
119
+ Args:
120
+ sequence_0: Sequence of Bernoulli data from the first source.
121
+ sequence_1: Sequence of Bernoulli data from the second source.
122
+
123
+ Returns:
124
+ TestResult: Result of the hypothesis test.
125
+ """
126
+ result_for_alternative = self._test_for_alternative.run_on_sequence(
127
+ sequence_0, sequence_1
128
+ )
129
+ result_for_null = self._test_for_null.run_on_sequence(sequence_0, sequence_1)
130
+
131
+ info = {
132
+ "result_for_alternative": result_for_alternative,
133
+ "result_for_null": result_for_null,
134
+ }
135
+
136
+ if (not result_for_alternative.decision == Decision.FailToDecide) and (
137
+ result_for_null.decision == Decision.FailToDecide
138
+ ):
139
+ decision = Decision.AcceptAlternative
140
+ elif (not result_for_null.decision == Decision.FailToDecide) and (
141
+ result_for_alternative.decision == Decision.FailToDecide
142
+ ):
143
+ decision = Decision.AcceptNull
144
+ else:
145
+ decision = Decision.FailToDecide
146
+
147
+ result = TestResult(decision, info)
148
+
149
+ return result