Skip to content

Source feature clipping

neureptrace.decoding.source_clipping implements source-only feature clipping for robust cross-subject decoding.

The protocol is Category 1 / strict source-only. Lower and upper bounds are estimated from source rows only, then applied to source and held-out feature matrices. Held-out rows are never used to fit the clipping bounds.

Typical usage:

from neureptrace.decoding.source_clipping import fit_source_feature_clipping

result = fit_source_feature_clipping(
    source_features=X_source,
    test_features=X_target,
    config={"lower_quantile": 0.01, "upper_quantile": 0.99},
)

X_source_clip = result.train_features
X_target_clip = result.test_features

neureptrace.decoding.source_clipping

SourceFeatureClippingConfig dataclass

Source code in src/neureptrace/decoding/source_clipping/__init__.py
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@dataclass(frozen=True, slots=True)
class SourceFeatureClippingConfig:
    lower_quantile: float = DEFAULT_LOWER_QUANTILE
    upper_quantile: float = DEFAULT_UPPER_QUANTILE
    copy: bool = True

    def __post_init__(self) -> None:
        lower = _quantile(self.lower_quantile, name="lower_quantile")
        upper = _quantile(self.upper_quantile, name="upper_quantile")
        if lower >= upper:
            raise ValueError("lower_quantile must be smaller than upper_quantile.")
        object.__setattr__(self, "lower_quantile", lower)
        object.__setattr__(self, "upper_quantile", upper)
        object.__setattr__(self, "copy", _bool_value(self.copy, name="copy"))

SourceFeatureClippingResult dataclass

Source code in src/neureptrace/decoding/source_clipping/__init__.py
31
32
33
34
35
36
37
@dataclass(frozen=True, slots=True)
class SourceFeatureClippingResult:
    train_features: np.ndarray
    test_features: np.ndarray
    lower_bounds: np.ndarray
    upper_bounds: np.ndarray
    metadata: dict[str, Any] = field(default_factory=dict)

fit_source_feature_clipping(*, source_features, test_features, config=None)

Source code in src/neureptrace/decoding/source_clipping/__init__.py
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
def fit_source_feature_clipping(
    *,
    source_features: Sequence[Sequence[float]] | np.ndarray,
    test_features: Sequence[Sequence[float]] | np.ndarray,
    config: SourceFeatureClippingConfig | Mapping[str, Any] | None = None,
) -> SourceFeatureClippingResult:
    cfg = source_feature_clipping_config() if config is None else _coerce_config(config)
    source = _matrix(source_features, name="source_features")
    test = _matrix(test_features, name="test_features")
    if source.shape[1] != test.shape[1]:
        raise ValueError("source_features and test_features must have the same feature width.")
    lower, upper = source_feature_clipping_bounds(
        source,
        lower_quantile=cfg.lower_quantile,
        upper_quantile=cfg.upper_quantile,
    )
    train = apply_feature_clipping(source, lower_bounds=lower, upper_bounds=upper, copy=cfg.copy)
    clipped = apply_feature_clipping(test, lower_bounds=lower, upper_bounds=upper, copy=cfg.copy)
    return SourceFeatureClippingResult(
        train.astype(np.float32, copy=False),
        clipped.astype(np.float32, copy=False),
        lower.astype(np.float32, copy=False),
        upper.astype(np.float32, copy=False),
        _metadata(cfg, source.shape[0], test.shape[0], source.shape[1]),
    )

source_feature_clipping_config(*, lower_quantile=DEFAULT_LOWER_QUANTILE, upper_quantile=DEFAULT_UPPER_QUANTILE, copy=True)

Source code in src/neureptrace/decoding/source_clipping/__init__.py
40
41
42
43
44
45
46
47
48
49
50
def source_feature_clipping_config(
    *,
    lower_quantile: float | str = DEFAULT_LOWER_QUANTILE,
    upper_quantile: float | str = DEFAULT_UPPER_QUANTILE,
    copy: bool | int | str = True,
) -> SourceFeatureClippingConfig:
    lower = _quantile(lower_quantile, name="lower_quantile")
    upper = _quantile(upper_quantile, name="upper_quantile")
    if lower >= upper:
        raise ValueError("lower_quantile must be smaller than upper_quantile.")
    return SourceFeatureClippingConfig(lower, upper, _bool_value(copy, name="copy"))

source_feature_clipping_bounds(source_features, *, lower_quantile=DEFAULT_LOWER_QUANTILE, upper_quantile=DEFAULT_UPPER_QUANTILE)

Source code in src/neureptrace/decoding/source_clipping/__init__.py
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
def source_feature_clipping_bounds(
    source_features: Sequence[Sequence[float]] | np.ndarray,
    *,
    lower_quantile: float = DEFAULT_LOWER_QUANTILE,
    upper_quantile: float = DEFAULT_UPPER_QUANTILE,
) -> tuple[np.ndarray, np.ndarray]:
    source = _matrix(source_features, name="source_features")
    lower = _quantile(lower_quantile, name="lower_quantile")
    upper = _quantile(upper_quantile, name="upper_quantile")
    if lower >= upper:
        raise ValueError("lower_quantile must be smaller than upper_quantile.")
    return (
        np.quantile(source, lower, axis=0).astype(float, copy=False),
        np.quantile(source, upper, axis=0).astype(float, copy=False),
    )

apply_feature_clipping(features, *, lower_bounds, upper_bounds, copy=True)

Source code in src/neureptrace/decoding/source_clipping/__init__.py
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
def apply_feature_clipping(
    features: Sequence[Sequence[float]] | np.ndarray,
    *,
    lower_bounds: Sequence[float] | np.ndarray,
    upper_bounds: Sequence[float] | np.ndarray,
    copy: bool | int | str = True,
) -> np.ndarray:
    matrix = _matrix(features, name="features")
    lower = np.asarray(lower_bounds, dtype=float).reshape(-1)
    upper = np.asarray(upper_bounds, dtype=float).reshape(-1)
    if lower.shape[0] != matrix.shape[1] or upper.shape[0] != matrix.shape[1]:
        raise ValueError("lower_bounds and upper_bounds must match the feature width.")
    if np.any(lower > upper):
        raise ValueError("lower_bounds cannot exceed upper_bounds.")
    target = matrix.copy() if _bool_value(copy, name="copy") else matrix
    return np.clip(target, lower, upper, out=target)