Skip to content

Test the cross-brain effect from evals

You have run encoding analyze a few times and have a handful of eval files. This guide turns them into an answer to the question hypline is built for: does one partner's model, driven by the shared conversation, predict the other partner's brain better than a mismatched model would? That comparison is a within-subject fit, a cross-brain fit, and a pseudo-dyad baseline, read side by side.

This is the applied companion to Reading an encoding result, which explains what a single eval's axes mean. Here we compare several.

Produce the evals to compare

This guide assumes each subject you score has a folded model trained on a semantic feature, tagged v1 — for the target and its partner, and for the out-of-dyad subjects used as a baseline below:

hypline encoding train data/ \
  --data-filters task-conv --features semantic --desc v1 \
  --fold-by run

Two details carry into everything that follows. The band is named after the feature ref, so training --features semantic gives a band called semantic; if you train --features semantic-gpt3, select band="semantic-gpt3" below instead. And the model must be folded, because the analyses use the default out-of-sample scoring — an unfolded model has no held-out runs to score and raises.

Each condition is then one analyze run against the same target brain, varying only the source and model:

# within-brain: the subject's own model and speech
hypline encoding analyze data/ \
  --target-sub 031 --source-sub self --model-sub self \
  --model-desc v1 --desc within

# cross-brain, self-driven: own speech, partner's model
hypline encoding analyze data/ \
  --target-sub 031 --source-sub self --model-sub partner \
  --model-desc v1 --desc crossself

# pseudo-dyad: own speech and brain, an out-of-dyad model (named by ID)
hypline encoding analyze data/ \
  --target-sub 031 --source-sub self --model-sub 045 \
  --model-desc v1 --desc pseudodyad

The three land under results/sub-031/ as encodingEval-within, encodingEval-crossself, and encodingEval-pseudodyad. Which source/model pairings mean what is laid out in Choosing source and model.

Reduce each eval to one number per voxel

Load each eval and collapse it to a comparable summary: pick the feature band and the role, then average over folds. For a cross-brain question during the target's own speech, that is the semantic band on the prod role:

from hypline.encoding import load_eval

def prod_semantic(desc):
    ds = load_eval(f"data/results/sub-031/encodingEval-{desc}/sub-031_result-encodingEval_desc-{desc}.nc")
    return ds["corr"].sel(band="semantic", role="prod").mean("fold", skipna=True)

within = prod_semantic("within")
crossself = prod_semantic("crossself")
pseudo = prod_semantic("pseudodyad")

Each result is one score per voxel. skipna=True matters here: a fold where the role had no rows scored NaN, and a plain mean would propagate it — see the fold axis.

Compare against the baseline

The pseudo-dyad eval is the reference for chance. The cross-brain effect is real to the extent the self-driven cross-brain score clears it:

# per-voxel margin of the cross-brain fit over the pseudo-dyad baseline
cross_margin = crossself - pseudo

# a whole-brain summary: fraction of scored voxels the cross-brain fit beats
# baseline on (drop voxels that are NaN in either eval first)
valid = cross_margin.notnull()
(cross_margin.where(valid) > 0).sum().item() / valid.sum().item()

A within-brain score above both is the sanity check that the feature encodes in this brain at all; the cross-brain margin over pseudo-dyad is the effect of interest.

Compare one band, don't read the sum as model quality

These are himalaya split scores: they decompose the joint correlation, so summing the feature and confound bands does recover the whole model's score — see What the scores are. But that joint number is not the comparison here. The cross-brain question is about the semantic band specifically, so compare that one band across conditions, as above, rather than folding in bands that are not what you are testing.

Strengthen the baseline

One out-of-dyad subject is one draw. For a real null you want a distribution: run the pseudo-dyad analysis against several out-of-dyad subjects and compare the cross-brain score against that spread rather than a single value.

import numpy as np

pseudo_ids = ["045", "047", "052", "058"]   # subjects outside 031's dyad
for sid in pseudo_ids:
    ...  # analyze with --model-sub <sid> --desc pseudo<sid>, then load and reduce

baseline = np.stack([prod_semantic(f"pseudo{sid}") for sid in pseudo_ids])
baseline_mean = baseline.mean(0)   # per-voxel chance level across the null set

The wider the null set, the more trustworthy the claim that a cross-brain score sits above chance. Log how many out-of-dyad subjects you drew, since the strength of the baseline is part of the result.