Between-condition dissimilarity with bootstrap confidence intervals
Source:R/rel_dissimilarity.R
rel_dissimilarity.RdQuantifies overall dissimilarity between two conditions'
group-level classification images. The primary statistic is
Euclidean distance between the two group-mean CIs, reported
both raw and normalised by sqrt(n_pixels) for cross-resolution
comparability. Percentile bootstrap 95% confidence intervals are
computed by resampling participants with replacement within each
condition.
Pair with rel_cluster_test() when you also want to know
where the two conditions differ.
Usage
rel_dissimilarity(
signal_matrix_a,
signal_matrix_b,
paired = FALSE,
n_boot = 2000L,
ci_level = 0.95,
null = c("none", "permutation"),
n_permutations = 2000L,
mask = NULL,
seed = NULL,
progress = TRUE,
acknowledge_scaling = FALSE
)Arguments
- signal_matrix_a, signal_matrix_b
Pixels x participants, base-subtracted. Row counts must match.
- paired
Logical.
FALSE(default) for between-subjects: participants resampled within A and B independently.TRUEfor within-subjects: A and B share a single resample index per replicate so the paired covariance structure is preserved.- n_boot
Bootstrap replicates. Default 2000.
- ci_level
Confidence level. Default 0.95.
- null
One of
"none"(default) or"permutation". Set"permutation"for any above-chance claim about the distance: it builds an empirical chance baseline by shuffling condition labels (or sign-flipping paired differences) and recomputing the distance. The default bootstrap CI is a precision interval only, not a test against zero (see the section on why a bootstrap CI is not a test against zero).- n_permutations
Integer. Number of null iterations when
null = "permutation". Default 2000.- mask
Optional logical vector of length
nrow(signal_matrix_a)(column-major) restricting the Euclidean / correlation computation to a region. Build withmake_face_mask()(parametric oval and sub-regions) orread_face_mask()(PNG/JPEG mask).- seed
Optional integer; RNG state restored on exit.
- progress
Show a
cliprogress bar.- acknowledge_scaling
Logical. When
FALSE(default), the sharedassert_raw_signal()helper errors on a known-rendered matrix on either side.
Details
For the observed statistics and each bootstrap replicate i:
mean_a = rowMeans(signal_matrix_a)
mean_b = rowMeans(signal_matrix_b)
observed_dist = sqrt(sum((mean_a - mean_b)^2))
observed_dist_normalised = observed_dist / sqrt(n_pixels)Percentile CI via base R quantile(); no boot dependency.
Why Euclidean and not Pearson correlation as the primary
Two base-subtracted CIs share systematic image-domain spatial
structure (face shape, oval signal support, low-frequency
Gaussian-noise smoothness) that pushes their correlation above
zero even when the underlying mental representations are
unrelated. Absolute correlation values therefore do not cleanly
mean "these conditions are similar": two unrelated traits can
easily produce r well above zero from the shared image
scaffolding alone, so a "high" correlation does not by itself
license a similarity claim. Euclidean distance does not share
this baseline issue, which is why it is the primary statistic
here.
Why a bootstrap CI on the L2 distance is not a test against zero
The Euclidean distance is a non-negative L2 norm of the difference
between two group means. Resampling producers with replacement adds
variance to each resampled group mean, and that variance enters the
squared-distance sum at every pixel, so
E[(mean_a_boot - mean_b_boot)^2] is roughly
(mean_a - mean_b)^2 + Var(mean_a_boot) + Var(mean_b_boot). Summed
over thousands of pixels the upward bias is large: the bootstrap
distribution of the distance sits above the observed value, and its
confidence interval almost always excludes zero even when the two
conditions are drawn from the same population. The bootstrap CI on
this distance therefore does not answer "is there a difference?". It
answers only "how stable is my distance estimate under producer
resampling?", and it is useful for comparing relative magnitudes
across contrasts where the bias is roughly constant.
To test whether the observed distance is larger than chance, set
null = "permutation". The permutation null shuffles producer
condition labels (or sign-flips per-producer differences in paired
designs), recomputes the distance, and repeats. Its distribution is
centred not at zero but at the chance distance between any two random
subgroups of producers, which is itself positive because each
subgroup mean carries within-group noise. Read the observed distance
against the upper tail of that distribution via $d_z, $d_ratio,
$d_null_p95, or a permutation p computed as the proportion of
$null_distribution values at or above $euclidean. The Pearson
fields carry the mirror-image bias: resampling attenuates
correlation, so the bootstrap CI on $correlation sits below the
observed value. Benchmark r against a permutation null as well,
never against zero.
The Pearson correlation fields ($correlation, $boot_cor,
$ci_cor, $boot_se_cor) are returned alongside it as a
secondary summary, for users who need to report r for
comparability with prior literature or with another analysis
pipeline. They are not recommended as a standalone similarity
score. If you do report r, interpret it relatively (the
ordering of r across multiple condition pairs is more
defensible than any single absolute value) and benchmark
against a permutation null rather than against zero. The
image-domain scaffolding shifts the chance baseline upward;
"above zero" is not the right comparison.
Reading the plot
plot() on the returned object renders the bootstrap
distributions as two side-by-side histograms (Euclidean
distance on the left, Pearson r on the right). The Pearson
panel is rendered in grey because it is a secondary summary,
not recommended as a standalone similarity score; see the
description for why and how to use it carefully if needed.
The shaded vertical band marks the percentile CI at
ci_level.The vertical line marks the observed statistic on the real data (not a bootstrap mean).
The Euclidean CI band is a precision interval on the distance estimate, not a test against zero. Producer resampling inflates the distance, so this band sits above the observed value and almost always excludes zero even for identical conditions; do not read "excludes zero" as evidence of a difference. For an above-chance test, run with
null = "permutation". The numbers are returned in$ci_dist.For visual comparison across multiple contrasts, pass each
rel_dissimilarity()result toplot_dissimilarity_grid(), which lays them out as labelled CI bars on a shared axis.For a spatial picture of where the two conditions differ, pair this with
rel_cluster_test()(or userun_discriminability()to run both in one call).
Reading the result
$euclidean, observed Euclidean distance between group means (primary statistic).$euclidean_normalised,$euclidean / sqrt(n_pixels). Use for cross-resolution comparisons.$boot_dist,$ci_dist,$boot_se_dist, bootstrap distribution, percentile CI, and SE of the Euclidean distance. The CI is a precision interval on the estimate, not a test against zero: it is biased upward by resampling and excludes zero even for identical conditions. Usenull = "permutation"to test against chance.$null(character), the null mode used.$null_distribution, whennull != "none": numeric vector of per-iteration Euclidean distances under the chosen null.$d_null_p95, 95th percentile of the null distribution.$d_z, z-equivalent effect size:(observed_d - mean(null)) / sd(null).$d_ratio, observed Euclidean over the null median.$correlation,$boot_cor,$ci_cor,$boot_se_cor: Pearson correlation of the group means and its bootstrap summaries. Secondary; not recommended as a standalone similarity score (see the "Why Euclidean" section). If reporting, prefer relative comparisons across pairs against a permutation null.$n_boot,$ci_level,$paired, metadata.
References
Efron, B., & Tibshirani, R. J. (1994). An introduction to the bootstrap. Chapman & Hall / CRC.
Examples
if (FALSE) { # \dontrun{
# Minimal call-signature demo with two synthetic inputs.
n_pix <- 32L * 32L
n_prod <- 20L
set.seed(1)
signal_matrix_a <- matrix(rnorm(n_pix * n_prod), n_pix, n_prod)
signal_matrix_b <- matrix(rnorm(n_pix * n_prod), n_pix, n_prod)
rel_dissimilarity(signal_matrix_a, signal_matrix_b,
n_boot = 200L, seed = 1)
} # }
if (FALSE) { # \dontrun{
# Same function, richer input: signal planted in different face regions
# (eyes vs mouth). The Euclidean distance and its bootstrap CI should
# be well above zero, reflecting genuine spatial divergence.
sim_eyes <- simulate_briefrc_data(
n_per_condition = 20, n_trials = 60, conditions = "x",
signal_region = "eyes", signal_strength = "strong", seed = 1
)
sim_mouth <- simulate_briefrc_data(
n_per_condition = 20, n_trials = 60, conditions = "x",
signal_region = "mouth", signal_strength = "strong", seed = 2
)
sig_eyes <- ci_from_responses_briefrc(
sim_eyes$data, noise_matrix = sim_eyes$noise_matrix)$signal_matrix
sig_mouth <- ci_from_responses_briefrc(
sim_mouth$data, noise_matrix = sim_mouth$noise_matrix)$signal_matrix
d <- rel_dissimilarity(sig_eyes, sig_mouth, n_boot = 500L, seed = 1)
# Bootstrap distribution + observed Euclidean + 95% CI band.
plot(d, main = "Eyes vs Mouth: bootstrap dissimilarity")
} # }