Skip to content

Random subspace ensemble

neureptrace.decoding.random_subspace implements a strict source-only random-subspace ensemble for M/EEG feature matrices.

Each ensemble member is trained on a deterministic random subset of feature columns. Optional row bootstrapping is applied only to training rows. Test rows are only scored and never influence feature-subspace selection, row sampling, model fitting, or model weighting.

This is a Category 1 / strict source-only method.

Typical usage:

from neureptrace.decoding.random_subspace import fit_random_subspace_ensemble

result = fit_random_subspace_ensemble(
    train_features=X_source,
    train_labels=y_source,
    test_features=X_target,
    config={"n_estimators": 32, "feature_fraction": 0.5},
)

probabilities = result.probabilities
predictions = result.predictions

neureptrace.decoding.random_subspace

Source-only random-subspace ensembles for M/EEG decoding.

This module implements a dependency-light random-subspace ensemble. Each member is trained on a source-label fold using a deterministic random subset of feature columns, and held-out rows are only scored. No target rows influence fitting, feature selection, model weighting, or model selection.

RandomSubspaceEnsembleConfig dataclass

Configuration for a strict source-only random-subspace ensemble.

Source code in src/neureptrace/decoding/random_subspace.py
29
30
31
32
33
34
35
36
37
38
39
@dataclass(frozen=True, slots=True)
class RandomSubspaceEnsembleConfig:
    """Configuration for a strict source-only random-subspace ensemble."""

    n_estimators: int = DEFAULT_N_ESTIMATORS
    feature_fraction: float = DEFAULT_FEATURE_FRACTION
    min_features: int = 1
    bootstrap_rows: bool = False
    row_fraction: float = 1.0
    random_state: int | None = 13
    epsilon: float = DEFAULT_EPSILON

RandomSubspaceMember dataclass

One fitted ensemble member and its selected rows/features.

Source code in src/neureptrace/decoding/random_subspace.py
42
43
44
45
46
47
48
49
@dataclass(frozen=True, slots=True)
class RandomSubspaceMember:
    """One fitted ensemble member and its selected rows/features."""

    estimator_index: int
    model: BaseEstimator
    feature_indices: np.ndarray
    row_indices: np.ndarray

RandomSubspaceEnsembleResult dataclass

Predictions, probabilities, and provenance for the ensemble.

Source code in src/neureptrace/decoding/random_subspace.py
52
53
54
55
56
57
58
59
60
61
@dataclass(frozen=True, slots=True)
class RandomSubspaceEnsembleResult:
    """Predictions, probabilities, and provenance for the ensemble."""

    probabilities: np.ndarray
    predictions: np.ndarray
    classes: np.ndarray
    members: tuple[RandomSubspaceMember, ...]
    member_probabilities: tuple[np.ndarray, ...]
    metadata: dict[str, Any] = field(default_factory=dict)

fit_random_subspace_ensemble(*, train_features, train_labels, test_features, config=None, estimator=None, sample_weight=None)

Fit a strict source-only random-subspace ensemble and score test rows.

Parameters:

Name Type Description Default
train_features Sequence[Sequence[float]] | ndarray

Source/training rows and labels. Feature subsets and optional bootstrap row samples are drawn only from these rows.

required
train_labels Sequence[Sequence[float]] | ndarray

Source/training rows and labels. Feature subsets and optional bootstrap row samples are drawn only from these rows.

required
test_features Sequence[Sequence[float]] | ndarray

Rows to score after the ensemble is fitted. They are not used during fitting or feature-subspace selection.

required
config RandomSubspaceEnsembleConfig | Mapping[str, Any] | None

Ensemble settings. A mapping is normalized through :func:random_subspace_ensemble_config.

None
estimator BaseEstimator | None

Optional sklearn-compatible classifier. If omitted, a standardized logistic regression classifier is used.

None
sample_weight Sequence[float] | ndarray | None

Optional source/training sample weights. When bootstrapping rows, weights are subset to the sampled rows.

None

Returns:

Type Description
RandomSubspaceEnsembleResult

Averaged probabilities, predictions, fitted members, and protocol metadata.

Source code in src/neureptrace/decoding/random_subspace.py
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
def fit_random_subspace_ensemble(
    *,
    train_features: Sequence[Sequence[float]] | np.ndarray,
    train_labels: Sequence[Any] | np.ndarray,
    test_features: Sequence[Sequence[float]] | np.ndarray,
    config: RandomSubspaceEnsembleConfig | Mapping[str, Any] | None = None,
    estimator: BaseEstimator | None = None,
    sample_weight: Sequence[float] | np.ndarray | None = None,
) -> RandomSubspaceEnsembleResult:
    """Fit a strict source-only random-subspace ensemble and score test rows.

    Parameters
    ----------
    train_features, train_labels:
        Source/training rows and labels.  Feature subsets and optional bootstrap
        row samples are drawn only from these rows.
    test_features:
        Rows to score after the ensemble is fitted.  They are not used during
        fitting or feature-subspace selection.
    config:
        Ensemble settings.  A mapping is normalized through
        :func:`random_subspace_ensemble_config`.
    estimator:
        Optional sklearn-compatible classifier.  If omitted, a standardized
        logistic regression classifier is used.
    sample_weight:
        Optional source/training sample weights.  When bootstrapping rows, weights
        are subset to the sampled rows.

    Returns
    -------
    RandomSubspaceEnsembleResult
        Averaged probabilities, predictions, fitted members, and protocol
        metadata.
    """

    cfg = random_subspace_ensemble_config() if config is None else _coerce_config(config)
    train = _feature_matrix(train_features, name="train_features")
    test = _feature_matrix(test_features, name="test_features")
    if train.shape[1] != test.shape[1]:
        raise ValueError(f"train_features and test_features must have the same feature width: {train.shape[1]} != {test.shape[1]}.")
    labels = _label_vector(train_labels, expected_length=train.shape[0], name="train_labels")
    classes = _unique_labels_in_order(labels)
    if classes.shape[0] < 2:
        raise ValueError("Random-subspace ensemble requires at least two classes.")
    label_codes = np.asarray([_label_index(classes, class_label) for class_label in labels.tolist()], dtype=int)
    encoded_classes = np.arange(classes.shape[0], dtype=int)
    weights = None if sample_weight is None else _sample_weight(sample_weight, expected_length=train.shape[0])
    rng = np.random.default_rng(cfg.random_state)
    feature_subspaces = sample_feature_subspaces(
        n_features=train.shape[1],
        n_estimators=cfg.n_estimators,
        feature_fraction=cfg.feature_fraction,
        min_features=cfg.min_features,
        random_state=cfg.random_state,
    )
    model_template = _default_estimator(cfg) if estimator is None else estimator
    members: list[RandomSubspaceMember] = []
    member_probabilities: list[np.ndarray] = []
    for estimator_index, feature_indices in enumerate(feature_subspaces):
        row_indices = _row_indices(
            n_rows=train.shape[0],
            row_fraction=cfg.row_fraction,
            bootstrap_rows=cfg.bootstrap_rows,
            rng=rng,
        )
        if np.unique(label_codes[row_indices]).shape[0] < 2:
            row_indices = np.arange(train.shape[0], dtype=int)
        model = clone(model_template)
        row_weights = None if weights is None else weights[row_indices]
        _fit_model(model, train[row_indices][:, feature_indices], label_codes[row_indices], sample_weight=row_weights)
        probabilities = _aligned_probabilities(model, test[:, feature_indices], classes=encoded_classes, epsilon=cfg.epsilon)
        members.append(
            RandomSubspaceMember(
                estimator_index=int(estimator_index),
                model=model,
                feature_indices=feature_indices.astype(int, copy=True),
                row_indices=row_indices.astype(int, copy=True),
            )
        )
        member_probabilities.append(probabilities)
    averaged = _normalize_probability_rows(np.mean(np.stack(member_probabilities, axis=0), axis=0), epsilon=cfg.epsilon)
    predictions = classes[np.argmax(averaged, axis=1)]
    metadata = _metadata(
        cfg,
        n_train_rows=train.shape[0],
        n_test_rows=test.shape[0],
        feature_dim=train.shape[1],
        n_classes=classes.shape[0],
        member_feature_counts=[member.feature_indices.shape[0] for member in members],
        member_row_counts=[member.row_indices.shape[0] for member in members],
    )
    return RandomSubspaceEnsembleResult(
        probabilities=averaged.astype(np.float32, copy=False),
        predictions=predictions,
        classes=classes,
        members=tuple(members),
        member_probabilities=tuple(probability.astype(np.float32, copy=False) for probability in member_probabilities),
        metadata=metadata,
    )

random_subspace_ensemble_config(*, n_estimators=DEFAULT_N_ESTIMATORS, feature_fraction=DEFAULT_FEATURE_FRACTION, min_features=1, bootstrap_rows=False, row_fraction=1.0, random_state=13, epsilon=DEFAULT_EPSILON)

Normalize public random-subspace ensemble options.

Source code in src/neureptrace/decoding/random_subspace.py
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
def random_subspace_ensemble_config(
    *,
    n_estimators: int | str = DEFAULT_N_ESTIMATORS,
    feature_fraction: float | str = DEFAULT_FEATURE_FRACTION,
    min_features: int | str = 1,
    bootstrap_rows: bool | str | int | float = False,
    row_fraction: float | str = 1.0,
    random_state: int | str | None = 13,
    epsilon: float | str = DEFAULT_EPSILON,
) -> RandomSubspaceEnsembleConfig:
    """Normalize public random-subspace ensemble options."""

    return RandomSubspaceEnsembleConfig(
        n_estimators=_positive_int(n_estimators, name="n_estimators"),
        feature_fraction=_unit_interval_float(feature_fraction, name="feature_fraction", include_zero=False),
        min_features=_positive_int(min_features, name="min_features"),
        bootstrap_rows=_bool_value(bootstrap_rows, name="bootstrap_rows"),
        row_fraction=_unit_interval_float(row_fraction, name="row_fraction", include_zero=False),
        random_state=_optional_random_state(random_state, name="random_state"),
        epsilon=_positive_float(epsilon, name="epsilon"),
    )

sample_feature_subspaces(*, n_features, n_estimators=DEFAULT_N_ESTIMATORS, feature_fraction=DEFAULT_FEATURE_FRACTION, min_features=1, random_state=13)

Return deterministic random feature-index subsets.

Source code in src/neureptrace/decoding/random_subspace.py
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
def sample_feature_subspaces(
    *,
    n_features: int | str,
    n_estimators: int | str = DEFAULT_N_ESTIMATORS,
    feature_fraction: float | str = DEFAULT_FEATURE_FRACTION,
    min_features: int | str = 1,
    random_state: int | str | None = 13,
) -> tuple[np.ndarray, ...]:
    """Return deterministic random feature-index subsets."""

    total_features = _positive_int(n_features, name="n_features")
    n_members = _positive_int(n_estimators, name="n_estimators")
    fraction = _unit_interval_float(feature_fraction, name="feature_fraction", include_zero=False)
    minimum = min(_positive_int(min_features, name="min_features"), total_features)
    subset_size = min(total_features, max(minimum, int(round(total_features * fraction))))
    seed = _optional_random_state(random_state, name="random_state")
    rng = np.random.default_rng(seed)
    return tuple(np.sort(rng.choice(total_features, size=subset_size, replace=False)).astype(int, copy=False) for _ in range(n_members))