Files
experiments-database/analysis/matlab/tdcs_lme.m
T
Experiments DB Dev 8a18c894dd feat(matlab): add Satterthwaite DF + honest random-slope test to LME reports
Every fitlme-based report (lme_*, paper_*, phase_*, and the variations'
analyze.m) now shows, per effect: residual-DF p, Satterthwaite-DF p, and --
for the interaction -- an HONEST test from a per-animal random-SLOPE model
(day|rat), whose Satterthwaite DF collapses toward the animal count.

New: tdcs_random_slope_interaction.m (shared helper). Wired into tdcs_lme,
tdcs_paper_lme, tdcs_phase_lme, variation_analyze; SUMMARY.csv gains
interaction_p_satt / interaction_p_rs. Regenerated all results/, variations/,
matched-effort outputs.

Key point this surfaces: Satterthwaite ~= residual on the random-INTERCEPT
model (the slope's error is at session level), so it does NOT fix
pseudoreplication; the random-slope model does. Effect: full-range mergeA2
interaction 0.009 -> 0.75 (collapses); unmerge_d0_5 0.015 -> 0.13 (n.s.);
the pooled-control early windows survive honestly (naive_a2_d0_5 0.001 ->
0.028; naive_boxa_d0_5 0.003 -> 0.036). Suite 42/42.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 15:45:19 -04:00

64 lines
2.8 KiB
Matlab

function L = tdcs_lme(S, cfg)
%TDCS_LME Linear mixed-effects "days x tDCS" model on the Box-B2 vs Box-A2 arm.
% L = TDCS_LME(S, CFG) subsets one scenario table S to the two anchor groups,
% codes a binary tDCS factor (Box-B2 = 1, Box-A2 = 0), and fits the LINEAR
% mixed model
%
% success ~ day * tDCS + (1|subject)
%
% replicating the published formulation (a linear mixed-effects model for the
% number of successful reaches with a days x tDCS interaction, a main effect
% of days, and a main effect of tDCS). `day` is raw and 0-indexed, and OUR
% day 0 corresponds to the paper's "Day 1", so the tDCS main effect is the
% group difference on Day 1 -- directly comparable to the paper's "equal on
% Day 1" test. (Do not re-index day to 1-based: that would move the main
% effect's evaluation point off Day 1.)
%
% L fields:
% .lme the LinearMixedModel object
% .nSubjects number of subjects (Box-A2 + Box-B2)
% .nObs number of sessions
% .interaction / .day / .tDCS effect structs, each with .estimate .se
% .t .df .p (from Coefficients) and .F .df1 .df2 .Fp (from
% ANOVA). For these single-df terms F == t^2 and Fp == p.
%
% NOTE: this is a LINEAR mixed model (Gaussian on the raw count), matching
% the paper's method, unlike the Poisson/Binomial GLMMs in tdcs_models.
T = S(ismember(S.group, {cfg.anchorLow, cfg.anchorHigh}), :);
T.group = removecats(T.group);
T.tDCS = double(T.group == cfg.anchorHigh); % Box-B2 = 1, Box-A2 = 0
lme = fitlme(T, 'success ~ day*tDCS + (1|subject)');
C = lme.Coefficients;
A = anova(lme);
As = anova(lme, 'DFMethod', 'satterthwaite'); % Satterthwaite denominator DF
L.lme = lme;
L.nSubjects = numel(unique(T.subject));
L.nObs = height(T);
L.maxDayA = max(T.day(T.group == cfg.anchorLow)); % last day Box-A2 has data
L.maxDayB = max(T.day(T.group == cfg.anchorHigh)); % last day Box-B2 has data
L.interaction = localTerm(C, A, As, 'day:tDCS');
L.day = localTerm(C, A, As, 'day');
L.tDCS = localTerm(C, A, As, 'tDCS');
% Honest test: refit with a per-subject random SLOPE (see
% tdcs_random_slope_interaction) so the interaction DF collapses toward n.
L.interRS = tdcs_random_slope_interaction(T, ...
'success ~ day*tDCS + (day|subject)', 'day:tDCS');
end
function e = localTerm(C, A, As, name)
%LOCALTERM Pull one term's coefficient (t/df/p), residual-DF ANOVA (F/df/p),
% and Satterthwaite denominator DF + p (.dfSatt, .pSatt).
ci = strcmp(C.Name, name);
ai = strcmp(A.Term, name);
si = strcmp(As.Term, name);
e = struct( ...
'estimate', C.Estimate(ci), 'se', C.SE(ci), ...
't', C.tStat(ci), 'df', C.DF(ci), 'p', C.pValue(ci), ...
'F', A.FStat(ai), 'df1', A.DF1(ai), 'df2', A.DF2(ai), 'Fp', A.pValue(ai), ...
'dfSatt', As.DF2(si), 'pSatt', As.pValue(si));
end