Skip to content

Transfer Component Analysis

neureptrace.decoding.transfer_component_analysis implements a dependency-light Category 2 / unlabeled target-adaptive Transfer Component Analysis (TCA) utility.

TCA learns a latent space from source features and unlabeled held-out target features, then a downstream classifier can be trained using source labels only. The public APIs intentionally have no target_labels argument.

Supported kernels:

  • linear
  • rbf with explicit gamma or gamma="median"

Typical use:

from neureptrace.decoding.transfer_component_analysis import fit_tca_transfer_classifier

result = fit_tca_transfer_classifier(
    source_features=X_source,
    source_labels=y_source,
    target_features=X_target_unlabeled,
    n_components=16,
    kernel="linear",
)

Protocol interpretation:

[ X_s, y_s, X_t \text{ are used}; \quad y_t \text{ is not used.} ]

neureptrace.decoding.transfer_component_analysis

Transfer Component Analysis for unlabeled target-adaptive decoding.

Transfer Component Analysis (TCA) learns a shared latent space by reducing the marginal distribution discrepancy between labeled source rows and unlabeled target rows. The downstream probe is trained only with source labels in that latent space. The public APIs in this module intentionally do not accept target labels.

TransferComponentAnalysisModel dataclass

Fitted TCA projection and provenance.

Source code in src/neureptrace/decoding/transfer_component_analysis.py
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@dataclass(frozen=True, slots=True)
class TransferComponentAnalysisModel:
    """Fitted TCA projection and provenance."""

    source_fit_features: np.ndarray
    target_fit_features: np.ndarray
    fit_features_standardized: np.ndarray
    projection: np.ndarray
    kernel: str
    gamma: float | None
    feature_mean: np.ndarray
    feature_scale: np.ndarray
    latent_mean: np.ndarray
    latent_scale: np.ndarray
    n_components: int
    regularization: float
    metadata: dict[str, Any] = field(default_factory=dict)

TransferComponentAnalysisResult dataclass

Source/target latent features and fitted TCA model.

Source code in src/neureptrace/decoding/transfer_component_analysis.py
48
49
50
51
52
53
54
55
@dataclass(frozen=True, slots=True)
class TransferComponentAnalysisResult:
    """Source/target latent features and fitted TCA model."""

    source_features: np.ndarray
    target_features: np.ndarray
    model: TransferComponentAnalysisModel
    metadata: dict[str, Any] = field(default_factory=dict)

TCATransferClassificationResult dataclass

Source-label classifier outputs after TCA feature transfer.

Source code in src/neureptrace/decoding/transfer_component_analysis.py
58
59
60
61
62
63
64
65
66
67
68
69
@dataclass(frozen=True, slots=True)
class TCATransferClassificationResult:
    """Source-label classifier outputs after TCA feature transfer."""

    source_features: np.ndarray
    target_features: np.ndarray
    predictions: np.ndarray
    probabilities: np.ndarray | None
    classes: np.ndarray
    model: TransferComponentAnalysisModel
    classifier: BaseEstimator
    metadata: dict[str, Any] = field(default_factory=dict)

transfer_component_analysis_features(source_features, target_features, *, n_components=DEFAULT_TCA_COMPONENTS, kernel='linear', regularization=DEFAULT_TCA_REGULARIZATION, gamma=DEFAULT_TCA_GAMMA, standardize=True, normalize_components=True, epsilon=DEFAULT_TCA_EPSILON)

Fit TCA and return source/target latent features.

Parameters:

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

Labeled source feature rows. Source labels are not needed by TCA itself.

required
target_features Sequence[Sequence[float]] | ndarray

Unlabeled held-out target feature rows used to estimate the target feature distribution. This makes the method Category 2 rather than strict source-only.

required
n_components int | str | None

Requested latent dimensionality. The effective dimensionality is capped by the number of source plus target rows.

DEFAULT_TCA_COMPONENTS
kernel str | None

"linear" or "rbf".

'linear'
regularization float | str

Positive ridge term in the generalized eigenproblem.

DEFAULT_TCA_REGULARIZATION
gamma float | str | None

RBF gamma. "median" uses the median pairwise squared distance among source plus target fit rows. Ignored for the linear kernel.

DEFAULT_TCA_GAMMA
standardize bool

If true, z-score features using source-plus-target feature statistics. This is still Category 2 because unlabeled target features contribute.

True
normalize_components bool

If true, z-score latent TCA components using source-plus-target latent statistics from the fit set.

True
epsilon float | str

Numerical floor for standard deviations and generalized-eigenproblem regularization.

DEFAULT_TCA_EPSILON

Returns:

Type Description
TransferComponentAnalysisResult

Source and target latent features plus a reusable fitted model.

Notes

This function intentionally has no target_labels argument. Source labels should be used only by the downstream classifier.

Source code in src/neureptrace/decoding/transfer_component_analysis.py
 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
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
def transfer_component_analysis_features(
    source_features: Sequence[Sequence[float]] | np.ndarray,
    target_features: Sequence[Sequence[float]] | np.ndarray,
    *,
    n_components: int | str | None = DEFAULT_TCA_COMPONENTS,
    kernel: str | None = "linear",
    regularization: float | str = DEFAULT_TCA_REGULARIZATION,
    gamma: float | str | None = DEFAULT_TCA_GAMMA,
    standardize: bool = True,
    normalize_components: bool = True,
    epsilon: float | str = DEFAULT_TCA_EPSILON,
) -> TransferComponentAnalysisResult:
    """Fit TCA and return source/target latent features.

    Parameters
    ----------
    source_features:
        Labeled source feature rows.  Source labels are not needed by TCA itself.
    target_features:
        Unlabeled held-out target feature rows used to estimate the target feature
        distribution.  This makes the method Category 2 rather than strict
        source-only.
    n_components:
        Requested latent dimensionality.  The effective dimensionality is capped
        by the number of source plus target rows.
    kernel:
        ``"linear"`` or ``"rbf"``.
    regularization:
        Positive ridge term in the generalized eigenproblem.
    gamma:
        RBF gamma.  ``"median"`` uses the median pairwise squared distance among
        source plus target fit rows.  Ignored for the linear kernel.
    standardize:
        If true, z-score features using source-plus-target feature statistics.
        This is still Category 2 because unlabeled target features contribute.
    normalize_components:
        If true, z-score latent TCA components using source-plus-target latent
        statistics from the fit set.
    epsilon:
        Numerical floor for standard deviations and generalized-eigenproblem
        regularization.

    Returns
    -------
    TransferComponentAnalysisResult
        Source and target latent features plus a reusable fitted model.

    Notes
    -----
    This function intentionally has no ``target_labels`` argument.  Source labels
    should be used only by the downstream classifier.
    """

    source = _feature_matrix(source_features, name="source_features")
    target = _feature_matrix(target_features, name="target_features")
    if source.shape[1] != target.shape[1]:
        raise ValueError(f"source_features and target_features must have the same feature width: {source.shape[1]} != {target.shape[1]}.")
    if source.shape[0] < 1 or target.shape[0] < 1:
        raise ValueError("TCA requires at least one source row and one target row.")

    kernel_name = normalize_tca_kernel(kernel)
    reg = _positive_float(regularization, name="regularization")
    eps = _positive_float(epsilon, name="epsilon")
    combined = np.vstack([source, target])
    standardized, feature_mean, feature_scale = _standardize_fit_features(combined, enabled=bool(standardize), epsilon=eps)
    resolved_gamma = _resolve_gamma(standardized, gamma=gamma, kernel=kernel_name, epsilon=eps)
    k_matrix = _kernel_matrix(standardized, standardized, kernel=kernel_name, gamma=resolved_gamma)
    n_total = combined.shape[0]
    n_source = source.shape[0]
    n_target = target.shape[0]
    n_components_resolved = _effective_components(n_components, max_components=n_total)

    mmd_matrix = _mmd_matrix(n_source, n_target)
    centering = np.eye(n_total, dtype=float) - np.full((n_total, n_total), 1.0 / n_total, dtype=float)
    left = k_matrix @ mmd_matrix @ k_matrix + reg * np.eye(n_total, dtype=float)
    right = k_matrix @ centering @ k_matrix + eps * np.eye(n_total, dtype=float)
    eigenvalues, eigenvectors = eigh(_symmetrize(left), _symmetrize(right), check_finite=True)
    order = np.argsort(eigenvalues)
    projection = eigenvectors[:, order[:n_components_resolved]]
    latent = k_matrix @ projection
    latent, latent_mean, latent_scale = _normalize_latent(latent, enabled=bool(normalize_components), epsilon=eps)

    source_latent = latent[:n_source]
    target_latent = latent[n_source:]
    metadata = _metadata(
        n_source_rows=n_source,
        n_target_rows=n_target,
        feature_dim=source.shape[1],
        n_components=n_components_resolved,
        requested_components=n_components,
        kernel=kernel_name,
        gamma=resolved_gamma,
        regularization=reg,
        standardize=bool(standardize),
        normalize_components=bool(normalize_components),
        eigenvalues=eigenvalues[order[:n_components_resolved]],
        source_latent=source_latent,
        target_latent=target_latent,
    )
    model = TransferComponentAnalysisModel(
        source_fit_features=source.astype(np.float32, copy=False),
        target_fit_features=target.astype(np.float32, copy=False),
        fit_features_standardized=standardized.astype(np.float32, copy=False),
        projection=projection.astype(np.float32, copy=False),
        kernel=kernel_name,
        gamma=resolved_gamma,
        feature_mean=feature_mean.astype(np.float32, copy=False),
        feature_scale=feature_scale.astype(np.float32, copy=False),
        latent_mean=latent_mean.astype(np.float32, copy=False),
        latent_scale=latent_scale.astype(np.float32, copy=False),
        n_components=n_components_resolved,
        regularization=reg,
        metadata=metadata,
    )
    return TransferComponentAnalysisResult(
        source_features=source_latent.astype(np.float32, copy=False),
        target_features=target_latent.astype(np.float32, copy=False),
        model=model,
        metadata=metadata,
    )

transform_with_tca_model(model, features)

Project new rows with an already fitted TCA model.

Source code in src/neureptrace/decoding/transfer_component_analysis.py
196
197
198
199
200
201
202
203
204
205
206
def transform_with_tca_model(model: TransferComponentAnalysisModel, features: Sequence[Sequence[float]] | np.ndarray) -> np.ndarray:
    """Project new rows with an already fitted TCA model."""

    matrix = _feature_matrix(features, name="features")
    if matrix.shape[1] != model.feature_mean.shape[0]:
        raise ValueError(f"features width {matrix.shape[1]} does not match fitted width {model.feature_mean.shape[0]}.")
    standardized = (matrix - model.feature_mean) / model.feature_scale
    k_new = _kernel_matrix(standardized, model.fit_features_standardized, kernel=model.kernel, gamma=model.gamma)
    latent = k_new @ model.projection
    latent = (latent - model.latent_mean) / model.latent_scale
    return latent.astype(np.float32, copy=False)

fit_tca_transfer_classifier(*, source_features, source_labels, target_features, classifier=None, classifier_C=1.0, classifier_max_iter=1000, classifier_class_weight='balanced', sample_weight=None, n_components=DEFAULT_TCA_COMPONENTS, kernel='linear', regularization=DEFAULT_TCA_REGULARIZATION, gamma=DEFAULT_TCA_GAMMA, standardize=True, normalize_components=True, epsilon=DEFAULT_TCA_EPSILON)

Train a source-label classifier after Category-2 TCA alignment.

Source code in src/neureptrace/decoding/transfer_component_analysis.py
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
def fit_tca_transfer_classifier(
    *,
    source_features: Sequence[Sequence[float]] | np.ndarray,
    source_labels: Sequence[Any] | np.ndarray,
    target_features: Sequence[Sequence[float]] | np.ndarray,
    classifier: BaseEstimator | None = None,
    classifier_C: float | str = 1.0,
    classifier_max_iter: int | str = 1000,
    classifier_class_weight: str | Mapping[Any, float] | None = "balanced",
    sample_weight: Sequence[float] | np.ndarray | None = None,
    n_components: int | str | None = DEFAULT_TCA_COMPONENTS,
    kernel: str | None = "linear",
    regularization: float | str = DEFAULT_TCA_REGULARIZATION,
    gamma: float | str | None = DEFAULT_TCA_GAMMA,
    standardize: bool = True,
    normalize_components: bool = True,
    epsilon: float | str = DEFAULT_TCA_EPSILON,
) -> TCATransferClassificationResult:
    """Train a source-label classifier after Category-2 TCA alignment."""

    labels = _label_vector(source_labels)
    source = _feature_matrix(source_features, name="source_features")
    if labels.shape[0] != source.shape[0]:
        raise ValueError(f"source_labels must contain one label per source row: {labels.shape[0]} != {source.shape[0]}.")
    classes = _unique_labels(labels)
    if classes.shape[0] < 2:
        raise ValueError("source_labels must contain at least two classes.")
    weights = None if sample_weight is None else np.asarray(sample_weight, dtype=float).reshape(-1)
    if weights is not None:
        if weights.shape[0] != labels.shape[0]:
            raise ValueError(f"sample_weight must contain one value per source row: {weights.shape[0]} != {labels.shape[0]}.")
        if not np.all(np.isfinite(weights)) or np.any(weights < 0.0):
            raise ValueError("sample_weight must contain finite non-negative values.")

    tca = transfer_component_analysis_features(
        source,
        target_features,
        n_components=n_components,
        kernel=kernel,
        regularization=regularization,
        gamma=gamma,
        standardize=standardize,
        normalize_components=normalize_components,
        epsilon=epsilon,
    )
    model = clone(classifier) if classifier is not None else LogisticRegression(
        C=_positive_float(classifier_C, name="classifier_C"),
        max_iter=_positive_int(classifier_max_iter, name="classifier_max_iter"),
        class_weight=classifier_class_weight,
        random_state=13,
    )
    fit_kwargs = {} if weights is None else {"sample_weight": weights}
    model.fit(tca.source_features, labels, **fit_kwargs)
    predictions = _label_vector(model.predict(tca.target_features))
    probabilities = _predict_probabilities_or_none(model, tca.target_features)
    classes = _classes_from_model(model, fallback=classes)
    metadata = {
        **tca.metadata,
        "tca_classifier": type(model).__name__,
        "tca_classifier_uses_source_labels": True,
        "tca_classifier_uses_target_labels": False,
        "tca_classifier_n_classes": int(classes.shape[0]),
    }
    return TCATransferClassificationResult(
        source_features=tca.source_features,
        target_features=tca.target_features,
        predictions=predictions,
        probabilities=probabilities,
        classes=classes,
        model=tca.model,
        classifier=model,
        metadata=metadata,
    )

normalize_tca_kernel(kernel)

Normalize public aliases for TCA kernels.

Source code in src/neureptrace/decoding/transfer_component_analysis.py
286
287
288
289
290
291
292
293
def normalize_tca_kernel(kernel: str | None) -> str:
    """Normalize public aliases for TCA kernels."""

    normalized = "linear" if kernel is None else str(kernel).strip().lower().replace("-", "_")
    normalized = {"lin": "linear", "rbf_kernel": "rbf", "gaussian": "rbf", "gaussian_rbf": "rbf"}.get(normalized, normalized)
    if normalized not in TCA_KERNELS:
        raise ValueError(f"Unknown TCA kernel {kernel!r}. Available kernels: {', '.join(TCA_KERNELS)}.")
    return normalized