blockperm 0.1.0__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,78 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: blockperm
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Block permutation test for cross-validated decoding when labels follow recording blocks
|
|
5
|
+
Author: Dongjin Choi
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/jinchoi7651507/block-permutation-decoding
|
|
8
|
+
Requires-Python: >=3.12
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Dist: numpy>=2.0
|
|
12
|
+
Requires-Dist: scikit-learn>=1.4
|
|
13
|
+
Provides-Extra: test
|
|
14
|
+
Requires-Dist: pytest; extra == "test"
|
|
15
|
+
Dynamic: license-file
|
|
16
|
+
|
|
17
|
+
# blockperm: a block permutation test for cross-validated decoding
|
|
18
|
+
|
|
19
|
+
`block_permutation_test_score` tests a cross-validated decoding score when the labels follow recording blocks (for example conditions presented in blocks of consecutive trials) and training and test trials may come from the same blocks. It has the call pattern of scikit-learn's `permutation_test_score`:
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from blockperm import block_permutation_test_score
|
|
23
|
+
|
|
24
|
+
score, null_scores, p = block_permutation_test_score(
|
|
25
|
+
pipeline, X, y, blocks=block_id, cv=KFold(5, shuffle=True, random_state=0), n_permutations=1000, random_state=0)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## What it does
|
|
29
|
+
- Each permutation permutes the labels of whole blocks between blocks, so the labels of training and test trials move together and every block keeps its trials.
|
|
30
|
+
- The whole cross-validation is rerun for every permutation, including every step fitted to the labels. Put scaling, feature or neuron selection and the model in one scikit-learn `Pipeline`.
|
|
31
|
+
- `strata` restricts the permutation to blocks within the same stratum (for example within a run or a session).
|
|
32
|
+
- `draw` replaces the permutation with a user-given generator of block-label vectors, for example pseudosessions drawn from the task's own block generator.
|
|
33
|
+
- With few blocks the function enumerates every distinct arrangement (`exact="auto"`) and warns about the smallest attainable p value. With two blocks per class there are six arrangements, but for a two-class decoder an arrangement and its class swap give the same score, so p cannot fall below about one in three.
|
|
34
|
+
|
|
35
|
+
## When it is valid
|
|
36
|
+
- The enumerated null is exact, and the sampled p value valid, when the observed assignment was drawn uniformly from the set the function permutes over: all arrangements of the block labels, or all arrangements within `strata` if the design randomized within strata (for example within runs). Permuting across a unit the design did not randomize over can reject too often. For a constrained randomization (counterbalancing, limits per run), pass the design's own generator as `draw`.
|
|
37
|
+
- With a fixed block order, the test assumes that blocks are exchangeable under the null. Slow drift that follows the order can break this assumption, and then the test can reject or be conservative. A design with random block order, or several blocks per condition in random order, avoids the problem.
|
|
38
|
+
- With `GroupKFold` and blocks of equal size, the block-to-fold map depends on the tie order of `numpy.argsort`, which differs between platforms (arm64 and x86 gave different maps for 24 blocks with the same versions). For a result that reruns identically elsewhere, pass explicit folds (a list of train/test index pairs) or a shuffled `GroupKFold` with a seed (scikit-learn 1.6 or later), and record the folds.
|
|
39
|
+
|
|
40
|
+
## Why not the defaults
|
|
41
|
+
In the analyses this function accompanies, three rules that toolboxes run were tested on labels with no information, assigned to recording blocks, with training and test trials sharing blocks:
|
|
42
|
+
- trial-level permutation with the cross-validation rerun (scikit-learn `permutation_test_score`, MVPA-Light);
|
|
43
|
+
- trial-level permutation within runs (the CoSMoMVPA tutorial rule);
|
|
44
|
+
- permutation of training labels only, with test labels kept (the PyMVPA tutorial rule).
|
|
45
|
+
|
|
46
|
+
All three rejected far more often than 5%. Permuting block labels and rerunning the whole cross-validation stayed near 5% in random block designs. With a fixed block order the result depends on the data. In the simulated fixed orders the test was conservative (0–2.4% with shuffled 5-fold). In resting EEG with fixed orders aligned with slow drift it rejected often (first half against second half in most recordings, a pairs-of-blocks order in 29%), and it stayed near 5% for an order orthogonal to a linear trend. There the blocks were not exchangeable (label = time; see "When it is valid"). With labels that vary trial by trial, the first two rules stayed near 5%, and the training-label rule was somewhat above it (10–12%); the large excess arises when labels follow blocks. `examples/fake_block_labels.py` shows the effect on a synthetic drifting recording.
|
|
47
|
+
|
|
48
|
+
## One-line changes per tool
|
|
49
|
+
- **scikit-learn:** call `block_permutation_test_score` with `blocks=`. Passing `groups=` to `permutation_test_score` restricts the shuffle to within groups; with labels constant within blocks, every permutation then returns the observed labels.
|
|
50
|
+
- **CoSMoMVPA / PyMVPA:** permute chunk-level labels across chunks (one label per chunk, all trials of a chunk together) within the unit the design randomized over (within runs if blocks were randomized within runs; `strata=` here), and rerun the cross-validation.
|
|
51
|
+
- **PyMVPA training-label permutation:** permute the labels of all trials, training and test, block by block.
|
|
52
|
+
|
|
53
|
+
## Install and test
|
|
54
|
+
Python 3.12 with numpy, scipy and scikit-learn (tested with numpy 2.5.4, scipy 1.18.1, scikit-learn 1.7.2); tested on macOS (arm64). No special hardware is needed; one CPU core is enough.
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
pip install blockperm # from PyPI; or, in this folder, pip install ".[test]" (with pytest), or copy blockperm.py into a project
|
|
58
|
+
python -m pytest tests # a few minutes on one core; the warnings it prints are expected
|
|
59
|
+
python examples/fake_block_labels.py
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
On a laptop (Apple arm64, 2026-11-10) the install took about 15 s with a network connection, the tests about 1.5 min and the example about 1.5 min. In this folder alone the tests end with "8 passed, 4 skipped" (the equivalence tests need the reference file named below); inside the paper's repository they end with "12 passed". The example prints:
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
labels with no information, 40 recordings, p < 0.05:
|
|
66
|
+
permutation_test_score (trial labels permuted): 14 (35%)
|
|
67
|
+
block_permutation_test_score (block labels permuted): 1 (2%)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The test `test_equivalence_with_reference` checks that, with `exact=False`, the same `random_state` Generator and the same folds, this function returns the same observed score, null scores and p value (except that it returns no null when the observed score is undefined) as the reference implementation used in the analyses (`analyses/step2-comparison/code/sim.py` in the paper's repository; another location can be given with `REFERENCE_SIM_DIR`), for shuffled 5-fold, re-split stratified 5-fold, GroupKFold by block and a user `draw`; it is skipped when that file is not present. With the default `exact="auto"`, designs with no more arrangements than `n_permutations` are enumerated instead (the null then has one score per arrangement).
|
|
71
|
+
|
|
72
|
+
## Tutorial, MATLAB and Octave
|
|
73
|
+
- `docs/TUTORIAL.md`: a step-by-step guide (folds, strata, pseudosessions, few blocks, fixed block orders, checking a pipeline with labels that carry no information).
|
|
74
|
+
- `matlab/`: a MATLAB and Octave port, `block_permutation_test.m`, which takes a function returning the cross-validated score for a label vector. This Python function is the reference; the port is checked against it in `matlab/crosscheck/` (`matlab/README.md`).
|
|
75
|
+
- On PyPI: `pip install blockperm` (version 0.1.0, the version used in the paper).
|
|
76
|
+
|
|
77
|
+
## Not included
|
|
78
|
+
No `n_jobs` and no fit parameters (unlike `permutation_test_score`); the permutations run one after another. License: MIT (`LICENSE`).
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
blockperm.py,sha256=Sl-ACeHUnVvx4wy0egjyX3RecSvMmbaQRW3RwggfSgg,11870
|
|
2
|
+
blockperm-0.1.0.dist-info/licenses/LICENSE,sha256=zjTbufg5fIhUxUGFbaqKkdubSADuzu6GUXpjnZrIe5Q,1069
|
|
3
|
+
blockperm-0.1.0.dist-info/METADATA,sha256=Jt6xm_6nRI--334q7TjD73MPPh7yjZWpZa7KkOYa3eQ,7762
|
|
4
|
+
blockperm-0.1.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
5
|
+
blockperm-0.1.0.dist-info/top_level.txt,sha256=K9UXz8iJ__MAVlrXiMW63JX8UR3VCPco-8SyxkrBW6o,10
|
|
6
|
+
blockperm-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dongjin Choi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
blockperm
|
blockperm.py
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"""Block permutation test for cross-validated decoding when labels follow recording blocks.
|
|
2
|
+
|
|
3
|
+
`block_permutation_test_score` has the call pattern of scikit-learn's `permutation_test_score`. It differs in what
|
|
4
|
+
it permutes: the labels of whole blocks are permuted between blocks (training and held-out trials together), and
|
|
5
|
+
the whole cross-validation, including every step fitted to the labels, is rerun for every permutation. A label
|
|
6
|
+
vector can instead be drawn from a user-given generator (for example pseudosessions drawn from the task's own
|
|
7
|
+
block generator).
|
|
8
|
+
|
|
9
|
+
This is the block permutation rule of the accompanying analyses. The enumerated null is exact, and the sampled p
|
|
10
|
+
value valid, when the observed assignment was drawn uniformly from the set the function permutes over: all
|
|
11
|
+
arrangements of the block labels, or all arrangements within strata if the design randomised within strata. For a
|
|
12
|
+
constrained randomisation (counterbalancing, limits per run), pass the design's own generator as `draw`. For a
|
|
13
|
+
fixed block order the test rests on an exchangeability model, not on the randomisation.
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import math
|
|
18
|
+
import numbers
|
|
19
|
+
import warnings
|
|
20
|
+
from collections import Counter
|
|
21
|
+
|
|
22
|
+
import numpy as np
|
|
23
|
+
from sklearn.base import clone, is_classifier
|
|
24
|
+
from sklearn.metrics import check_scoring
|
|
25
|
+
from sklearn.model_selection import check_cv
|
|
26
|
+
|
|
27
|
+
__all__ = ["block_permutation_test_score", "block_labels_of", "n_distinct_arrangements"]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def block_labels_of(y, blocks):
|
|
31
|
+
"""Per-block labels (in the order of np.unique(blocks)) and the position of each trial's block.
|
|
32
|
+
|
|
33
|
+
Raises ValueError if a block holds more than one label."""
|
|
34
|
+
y = np.asarray(y)
|
|
35
|
+
blocks = np.asarray(blocks)
|
|
36
|
+
if y.shape[0] != blocks.shape[0]:
|
|
37
|
+
raise ValueError("y and blocks must have the same length")
|
|
38
|
+
ub = np.unique(blocks)
|
|
39
|
+
pos = np.searchsorted(ub, blocks)
|
|
40
|
+
lab = []
|
|
41
|
+
for i in range(len(ub)):
|
|
42
|
+
v = np.unique(y[pos == i])
|
|
43
|
+
if len(v) != 1:
|
|
44
|
+
raise ValueError("labels must be constant within blocks (block %r holds %d labels)" % (ub[i], len(v)))
|
|
45
|
+
lab.append(v[0])
|
|
46
|
+
return np.array(lab), pos
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def n_distinct_arrangements(block_labels, strata=None):
|
|
50
|
+
"""Number of distinct block-label vectors reachable by permuting labels between blocks (within strata).
|
|
51
|
+
Both arguments are per block (one value per block, in the order of np.unique(blocks))."""
|
|
52
|
+
block_labels = np.asarray(block_labels)
|
|
53
|
+
strata = None if strata is None else np.asarray(strata)
|
|
54
|
+
groups = [np.arange(len(block_labels))] if strata is None else [np.where(strata == s)[0] for s in np.unique(strata)]
|
|
55
|
+
n = 1
|
|
56
|
+
for g in groups:
|
|
57
|
+
c = Counter(block_labels[g].tolist())
|
|
58
|
+
k = math.factorial(len(g))
|
|
59
|
+
for v in c.values():
|
|
60
|
+
k //= math.factorial(v)
|
|
61
|
+
n *= k
|
|
62
|
+
return n
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _cv_score(estimator, X, y, splits, scorer, error_score):
|
|
66
|
+
scores = []
|
|
67
|
+
for tr, te in splits:
|
|
68
|
+
if is_classifier(estimator) and len(np.unique(y[tr])) < 2:
|
|
69
|
+
return error_score
|
|
70
|
+
m = clone(estimator).fit(X[tr], y[tr])
|
|
71
|
+
scores.append(scorer(m, X[te], y[te]))
|
|
72
|
+
return float(np.mean(scores))
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _splits(cv, X, y, groups):
|
|
76
|
+
with warnings.catch_warnings(): # splitters that ignore groups (KFold, StratifiedKFold) warn
|
|
77
|
+
warnings.filterwarnings("ignore", message="The groups parameter is ignored")
|
|
78
|
+
return list(cv.split(X, y, groups))
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def block_permutation_test_score(estimator, X, y, *, blocks, cv=None, n_permutations=1000, scoring=None,
|
|
82
|
+
random_state=None, strata=None, draw=None, groups="blocks", exact="auto",
|
|
83
|
+
error_score=np.nan):
|
|
84
|
+
"""Evaluate the significance of a cross-validated score by permuting labels between blocks.
|
|
85
|
+
|
|
86
|
+
Parameters
|
|
87
|
+
----------
|
|
88
|
+
estimator : scikit-learn estimator (a Pipeline with every label-fitted step: scaling, selection, model)
|
|
89
|
+
X : array (n_trials, n_features)
|
|
90
|
+
y : array (n_trials,), constant within each block
|
|
91
|
+
blocks : array (n_trials,), block identity of each trial (a recording block of consecutive trials)
|
|
92
|
+
cv : as in scikit-learn (int, splitter or iterable); default 5-fold. A splitter is re-applied to every
|
|
93
|
+
permuted label vector (as scikit-learn does), so label-stratified folds follow the permuted labels.
|
|
94
|
+
n_permutations : number of permutations (ignored when the exact enumeration is used)
|
|
95
|
+
scoring : as in scikit-learn; default the estimator's score (accuracy for classifiers). The score is the mean
|
|
96
|
+
over folds, as in `permutation_test_score`.
|
|
97
|
+
random_state : int, numpy Generator, RandomState or None (passed to numpy.random.default_rng unless a
|
|
98
|
+
Generator); the permutations use this stream only, so the same Generator and folds reproduce the same null
|
|
99
|
+
strata : optional array (n_trials,), constant within blocks: labels are permuted only between blocks of the
|
|
100
|
+
same stratum. Use the unit the design randomised over (for example within runs if blocks were randomised
|
|
101
|
+
within runs); permuting across that unit can reject too often.
|
|
102
|
+
draw : optional callable(rng) -> per-block label vector (order of np.unique(blocks)); replaces the
|
|
103
|
+
permutation, for example to draw pseudosessions from the task's generator. `strata` and `exact` are
|
|
104
|
+
ignored when `draw` is given.
|
|
105
|
+
groups : "blocks" (default) passes the block identity to `cv.split` as `groups`, so GroupKFold-type splitters
|
|
106
|
+
hold out whole blocks; None passes nothing; an array is passed as given.
|
|
107
|
+
exact : "auto" enumerates all distinct block-label arrangements when there are no more of them than
|
|
108
|
+
`n_permutations` (the null then has one score per arrangement, the observed one included, and its length
|
|
109
|
+
is the number of arrangements, not `n_permutations`); True forces the enumeration (at most 100,000
|
|
110
|
+
arrangements); False always samples with replacement.
|
|
111
|
+
error_score : number used as the score of a cross-validation with a one-class training fold (classifiers).
|
|
112
|
+
Other fit errors are raised. NaN null scores count as exceedances (conservative); an undefined observed
|
|
113
|
+
score returns p = NaN with a warning.
|
|
114
|
+
|
|
115
|
+
Returns
|
|
116
|
+
-------
|
|
117
|
+
score : float, observed cross-validated score
|
|
118
|
+
permutation_scores : array, scores under the null (empty when the observed score is undefined; the
|
|
119
|
+
reference implementation returned its full null in that case, with p NaN as here)
|
|
120
|
+
pvalue : float, (1 + #{null >= score} + #{null NaN}) / (n_null + 1) when sampling; when enumerating,
|
|
121
|
+
(1 + #{other arrangements with score >= observed or NaN}) / (number of arrangements), the observed
|
|
122
|
+
arrangement counted once with its observed score (also when the cross-validation is not deterministic).
|
|
123
|
+
"""
|
|
124
|
+
X = np.asarray(X)
|
|
125
|
+
y = np.asarray(y)
|
|
126
|
+
blocks = np.asarray(blocks)
|
|
127
|
+
if X.shape[0] != y.shape[0] or blocks.shape[0] != y.shape[0]:
|
|
128
|
+
raise ValueError("X, y and blocks must have the same number of trials")
|
|
129
|
+
if not (isinstance(exact, (bool, np.bool_)) or (isinstance(exact, str) and exact == "auto")):
|
|
130
|
+
raise ValueError('exact must be "auto", True or False')
|
|
131
|
+
exact = exact if isinstance(exact, str) else bool(exact)
|
|
132
|
+
if not isinstance(n_permutations, numbers.Integral) or isinstance(n_permutations, bool) or n_permutations < 1:
|
|
133
|
+
raise ValueError("n_permutations must be an integer of at least 1")
|
|
134
|
+
if isinstance(groups, str) and groups != "blocks":
|
|
135
|
+
raise ValueError('groups must be "blocks", None or an array with one value per trial')
|
|
136
|
+
if not isinstance(error_score, numbers.Real) or isinstance(error_score, (bool, np.bool_)):
|
|
137
|
+
raise ValueError("error_score must be a number (NaN by default)")
|
|
138
|
+
lab, pos = block_labels_of(y, blocks)
|
|
139
|
+
rng = random_state if isinstance(random_state, np.random.Generator) else np.random.default_rng(random_state)
|
|
140
|
+
cv = check_cv(cv, y, classifier=is_classifier(estimator))
|
|
141
|
+
scorer = check_scoring(estimator, scoring=scoring)
|
|
142
|
+
g = blocks if isinstance(groups, str) else groups
|
|
143
|
+
if strata is not None:
|
|
144
|
+
if np.asarray(strata).shape[0] != y.shape[0]:
|
|
145
|
+
raise ValueError("strata must have one value per trial")
|
|
146
|
+
st_lab, _ = block_labels_of(np.asarray(strata), blocks)
|
|
147
|
+
else:
|
|
148
|
+
st_lab = None
|
|
149
|
+
|
|
150
|
+
score = _cv_score(estimator, X, y, _splits(cv, X, y, g), scorer, error_score)
|
|
151
|
+
if not np.isfinite(score):
|
|
152
|
+
warnings.warn("the observed score is undefined (a training fold with one class, or a scorer that returned "
|
|
153
|
+
"NaN); p is NaN and no null was computed", stacklevel=2)
|
|
154
|
+
return score, np.array([], float), float("nan")
|
|
155
|
+
|
|
156
|
+
n_arr = n_distinct_arrangements(lab, st_lab) if draw is None else None
|
|
157
|
+
use_exact = draw is None and (exact is True or (exact == "auto" and n_arr <= n_permutations))
|
|
158
|
+
if draw is None and n_arr < 20:
|
|
159
|
+
warnings.warn("only %d distinct block-label arrangements: with exact enumeration p cannot fall below %.3g, "
|
|
160
|
+
"and not below %.3g when swapping the two classes leaves the score unchanged (a refit two-class "
|
|
161
|
+
"classifier with balanced blocks)%s"
|
|
162
|
+
% (n_arr, 1.0 / n_arr, 2.0 / n_arr, "" if use_exact else "; consider exact=True"), stacklevel=2)
|
|
163
|
+
|
|
164
|
+
if use_exact:
|
|
165
|
+
if n_arr > 100_000:
|
|
166
|
+
raise ValueError("too many arrangements to enumerate (%d)" % n_arr)
|
|
167
|
+
arrangements = _enumerate(lab, st_lab)
|
|
168
|
+
obs = next(i for i, a in enumerate(arrangements) if np.array_equal(a, lab))
|
|
169
|
+
null = np.array([score if i == obs else
|
|
170
|
+
_cv_score(estimator, X, a[pos], _splits(cv, X, a[pos], g), scorer, error_score)
|
|
171
|
+
for i, a in enumerate(arrangements)])
|
|
172
|
+
others = np.delete(null, obs)
|
|
173
|
+
ge = 1 + np.sum(others[~np.isnan(others)] >= score - 1e-12) + np.isnan(others).sum()
|
|
174
|
+
return score, null, float(ge / len(null))
|
|
175
|
+
|
|
176
|
+
null = []
|
|
177
|
+
for _ in range(n_permutations):
|
|
178
|
+
if draw is not None:
|
|
179
|
+
lp = np.asarray(draw(rng))
|
|
180
|
+
if lp.shape != lab.shape:
|
|
181
|
+
raise ValueError("draw must return one label per block")
|
|
182
|
+
elif st_lab is None:
|
|
183
|
+
lp = rng.permutation(lab)
|
|
184
|
+
else:
|
|
185
|
+
lp = lab.copy()
|
|
186
|
+
for s in np.unique(st_lab):
|
|
187
|
+
i = np.where(st_lab == s)[0]
|
|
188
|
+
lp[i] = lab[rng.permutation(i)]
|
|
189
|
+
yp = lp[pos]
|
|
190
|
+
null.append(_cv_score(estimator, X, yp, _splits(cv, X, yp, g), scorer, error_score))
|
|
191
|
+
null = np.array(null, float)
|
|
192
|
+
p = (1 + np.sum(null[~np.isnan(null)] >= score - 1e-12) + np.isnan(null).sum()) / (len(null) + 1)
|
|
193
|
+
return score, null, float(p)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _multiset_permutations(items):
|
|
197
|
+
"""Distinct orderings of a list with repeated values, each once (no n! intermediate list)."""
|
|
198
|
+
counts = Counter(items)
|
|
199
|
+
keys = sorted(counts)
|
|
200
|
+
n = len(items)
|
|
201
|
+
cur = []
|
|
202
|
+
|
|
203
|
+
def rec():
|
|
204
|
+
if len(cur) == n:
|
|
205
|
+
yield tuple(cur)
|
|
206
|
+
return
|
|
207
|
+
for k in keys:
|
|
208
|
+
if counts[k]:
|
|
209
|
+
counts[k] -= 1
|
|
210
|
+
cur.append(k)
|
|
211
|
+
yield from rec()
|
|
212
|
+
cur.pop()
|
|
213
|
+
counts[k] += 1
|
|
214
|
+
yield from rec()
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _enumerate(lab, st_lab):
|
|
218
|
+
"""All distinct block-label vectors reachable by permuting within strata (observed one included)."""
|
|
219
|
+
idx_groups = [np.arange(len(lab))] if st_lab is None else [np.where(st_lab == s)[0] for s in np.unique(st_lab)]
|
|
220
|
+
per_group = [list(_multiset_permutations(lab[i].tolist())) for i in idx_groups]
|
|
221
|
+
out = []
|
|
222
|
+
def rec(k, cur):
|
|
223
|
+
if k == len(idx_groups):
|
|
224
|
+
out.append(cur.copy())
|
|
225
|
+
return
|
|
226
|
+
for arr in per_group[k]:
|
|
227
|
+
cur[idx_groups[k]] = arr
|
|
228
|
+
rec(k + 1, cur)
|
|
229
|
+
rec(0, lab.copy())
|
|
230
|
+
return out
|