feat(matlab): windowed paper-LME replication for mergeNaive (d0_10, d0_13)
Make tdcs_paper_lme window-aware (windowKey 'full'|'d0_10'|'d0_13', default 'full' -> unchanged filename). Windowed runs give the two anchor arms equal day coverage, so the stim:day interaction is not confounded by the full-range coverage imbalance; the report now states whether coverage is equal and adds an INTERPRETATION block. Add switch cases paper_mergeNaive_d0_10 / _d0_13 and include them in run_all. Finding: unlike mergeA2 (fair-window interaction n.s.), the pooled-naive control keeps the interaction significant in the fair d0_13 window (F(1)=7.07, p=0.0088, +1.80 [+0.46,+3.14]; equal on Day 1 p=0.20), reproducing the paper without the coverage confound; d0_10 is borderline (p=0.051). Tests: tLme asserts equal coverage / no warning / one full summary for both windows and a significant positive d0_13 interaction; bad windowKey errors. Suite 41/41. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,23 +1,40 @@
|
||||
function L = tdcs_paper_lme(mergeKey, cfg)
|
||||
function L = tdcs_paper_lme(mergeKey, cfg, windowKey)
|
||||
%TDCS_PAPER_LME Replicate the paper's linear mixed model, verbatim formula:
|
||||
% behavior ~ stim + day + stim:day + (1|rat)
|
||||
% fit with fitlme on the Box-B2 (stim = 1, tDCS) vs Box-A2 (stim = 0, control)
|
||||
% subset of the MERGEKEY grouping ('unmerged'|'mergeA2'|'mergeB2'), over the
|
||||
% full training range. `behavior` = successful reaches, `day` = training day
|
||||
% (raw; day 0 = the paper's "Day 1"), `rat` = subject. Reports the three
|
||||
% effects (stim x day interaction, day, stim) as t(df) / F(1) / p with the
|
||||
% interaction 95% CI, alongside the paper's reference values, and writes
|
||||
% results/paper_<MERGEKEY>.txt.
|
||||
% subset of the MERGEKEY grouping ('unmerged'|'mergeA2'|'mergeB2'|'mergeNaive').
|
||||
% `behavior` = successful reaches (COUNT), `day` = training day (raw; day 0 =
|
||||
% the paper's "Day 1"), `rat` = subject. Reports the three effects (stim x day
|
||||
% interaction, day, stim) as t(df) / F(1) / p with the interaction 95% CI,
|
||||
% alongside the paper's reference values, and writes a results/ txt file.
|
||||
%
|
||||
% L = TDCS_PAPER_LME(MERGEKEY, CFG) uses the full training range (default) and
|
||||
% writes results/paper_<MERGEKEY>.txt.
|
||||
%
|
||||
% L = TDCS_PAPER_LME(MERGEKEY, CFG, WINDOWKEY) restricts to a day window --
|
||||
% WINDOWKEY is 'full' (day <= 26), 'd0_10' (day <= 10), or 'd0_13' (day <= 13,
|
||||
% the last day Box-A2 has data). The windowed fits give the two anchor groups
|
||||
% EQUAL day coverage, so the stim:day interaction is not confounded by the
|
||||
% full-range coverage imbalance; windowed runs write
|
||||
% results/paper_<MERGEKEY>_<WINDOWKEY>.txt.
|
||||
%
|
||||
% (This is the same model as tdcs_lme -- success ~ day*tDCS + (1|subject) --
|
||||
% written with the paper's exact term order and variable names.)
|
||||
%
|
||||
% L fields: .model .nRats .nObs and .stim/.day/.interaction effect structs
|
||||
% (.estimate .se .t .df .p from Coefficients; .F .df1 .df2 .Fp from ANOVA;
|
||||
% .ci = coefficient 95% CI).
|
||||
% L fields: .model .nRats .nObs .windowKey and .stim/.day/.interaction effect
|
||||
% structs (.estimate .se .t .df .p from Coefficients; .F .df1 .df2 .Fp from
|
||||
% ANOVA; .ci = coefficient 95% CI).
|
||||
|
||||
Sfull = tdcs_scenario_data([mergeKey '_full']);
|
||||
A = Sfull(ismember(Sfull.group, {cfg.anchorLow, cfg.anchorHigh}), :);
|
||||
if nargin < 3 || isempty(windowKey)
|
||||
windowKey = 'full';
|
||||
end
|
||||
if ~ismember(windowKey, {'full', 'd0_10', 'd0_13'})
|
||||
error('tdcs_paper_lme:badWindow', ...
|
||||
'windowKey must be ''full'', ''d0_10'', or ''d0_13'' (got "%s").', windowKey);
|
||||
end
|
||||
|
||||
S = tdcs_scenario_data([mergeKey '_' windowKey]);
|
||||
A = S(ismember(S.group, {cfg.anchorLow, cfg.anchorHigh}), :);
|
||||
|
||||
tbl = table();
|
||||
tbl.behavior = A.success;
|
||||
@@ -29,6 +46,7 @@ model = fitlme(tbl, 'behavior ~ stim + day + stim:day + (1|rat)');
|
||||
C = model.Coefficients; An = anova(model); ci = coefCI(model);
|
||||
|
||||
L.model = model;
|
||||
L.windowKey = windowKey;
|
||||
L.nRats = numel(unique(tbl.rat));
|
||||
L.nObs = height(tbl);
|
||||
L.maxDayCtrl = max(tbl.day(tbl.stim == 0));
|
||||
@@ -54,18 +72,32 @@ e = struct('estimate', C.Estimate(i), 'se', C.SE(i), 't', C.tStat(i), ...
|
||||
end
|
||||
|
||||
function localReport(L, mergeKey, cfg)
|
||||
window = L.windowKey;
|
||||
if strcmp(window, 'full')
|
||||
scenarioName = ['paper_' mergeKey];
|
||||
windowLabel = 'full range';
|
||||
else
|
||||
scenarioName = ['paper_' mergeKey '_' window];
|
||||
windowLabel = strrep(window, 'd0_10', 'days 0-10');
|
||||
windowLabel = strrep(windowLabel, 'd0_13', 'days 0-13');
|
||||
end
|
||||
|
||||
bar = repmat('=', 1, 78);
|
||||
s = sprintf('%s\n', bar);
|
||||
s = [s sprintf('PAPER LME REPLICATION -- %s\n', mergeKey)];
|
||||
s = [s sprintf('PAPER LME REPLICATION -- %s (%s)\n', mergeKey, windowLabel)];
|
||||
s = [s sprintf('%s\n', bar)];
|
||||
s = [s sprintf('model: behavior ~ stim + day + stim:day + (1|rat) [stim: %s=1 vs %s=0]\n', ...
|
||||
cfg.anchorHigh, cfg.anchorLow)];
|
||||
s = [s sprintf('behavior = successful reaches (COUNT per session)\n')];
|
||||
s = [s sprintf('N = %d rats, %d sessions (day raw; day 0 = paper "Day 1")\n', L.nRats, L.nObs)];
|
||||
s = [s sprintf('day coverage: stim(B2) 0..%d, control(A2) 0..%d\n', L.maxDayStim, L.maxDayCtrl)];
|
||||
if abs(L.maxDayStim - L.maxDayCtrl) > 2
|
||||
s = [s sprintf(['** WARNING: unequal day coverage -- the full-range stim:day interaction\n' ...
|
||||
' extrapolates the control group''s line and is CONFOUNDED here (the paper''s\n' ...
|
||||
' groups had equal coverage). See the _d0_13 fair-window and phased analyses. **\n'])];
|
||||
else
|
||||
s = [s sprintf(['(Equal day coverage -- the stim:day interaction over this window is NOT\n' ...
|
||||
' confounded by the full-range coverage imbalance.)\n'])];
|
||||
end
|
||||
s = [s sprintf('\n')];
|
||||
s = [s tdcs_model_summary(L.model, 'fitlme: behavior ~ stim + day + stim:day + (1|rat)') sprintf('\n')];
|
||||
@@ -75,6 +107,18 @@ s = [s localRow('stim x day (interaction)', L.interaction)];
|
||||
s = [s localRow('day (learning)', L.day)];
|
||||
s = [s localRow('stim (main, Day 1)', L.stim)];
|
||||
s = [s sprintf('interaction 95%% CI: [%+.2f, %+.2f]\n', L.interaction.ci(1), L.interaction.ci(2))];
|
||||
s = [s sprintf('\nINTERPRETATION\n')];
|
||||
if L.interaction.p >= 0.05
|
||||
interTxt = 'slopes parallel -- no differential learning rate over this window';
|
||||
elseif L.interaction.estimate > 0
|
||||
interTxt = 'tDCS (Box-B2) improves FASTER -- benefit accumulates over training';
|
||||
else
|
||||
interTxt = 'tDCS (Box-B2) improves SLOWER -- groups converge';
|
||||
end
|
||||
s = [s sprintf(' - stim x day interaction: %s (p=%.4f, slope diff=%+.2f) -> %s.\n', ...
|
||||
localSigTxt(L.interaction.p), L.interaction.p, L.interaction.estimate, interTxt)];
|
||||
s = [s sprintf(' - stim main effect on Day 1 (our day 0): %s (p=%.4f) -> groups %s on Day 1.\n', ...
|
||||
localSigTxt(L.stim.p), L.stim.p, localPick(L.stim.p < 0.05, 'already DIFFER', 'are comparable'))];
|
||||
s = [s sprintf(['\nPaper (N=24): interaction t(227)=2.68, F(1)=7.12, p=0.008; ' ...
|
||||
'day t(227)=9.64,\n F(1)=267.64, p=1.2e-18; stim t(227)=0.23, F(1)=0.053, p=0.81.\n'])];
|
||||
|
||||
@@ -82,7 +126,7 @@ fprintf('%s', s);
|
||||
thisDir = fileparts(mfilename('fullpath'));
|
||||
resDir = fullfile(thisDir, 'results');
|
||||
if ~exist(resDir, 'dir'); mkdir(resDir); end
|
||||
fid = fopen(fullfile(resDir, ['paper_' mergeKey '.txt']), 'w');
|
||||
fid = fopen(fullfile(resDir, [scenarioName '.txt']), 'w');
|
||||
if fid < 0; error('tdcs_paper_lme:fopen', 'Cannot open results file.'); end
|
||||
cleanup = onCleanup(@() fclose(fid)); %#ok<NASGU>
|
||||
fprintf(fid, '%s', s);
|
||||
@@ -91,3 +135,11 @@ end
|
||||
function r = localRow(name, e)
|
||||
r = sprintf('%-26s t(%d)=%6.2f F(%d)=%8.3f p=%.4g\n', name, e.df, e.t, e.df1, e.F, e.p);
|
||||
end
|
||||
|
||||
function t = localSigTxt(p)
|
||||
if p < 0.05; t = 'SIGNIFICANT'; else; t = 'n.s.'; end
|
||||
end
|
||||
|
||||
function t = localPick(b, yes, no)
|
||||
if b; t = yes; else; t = no; end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user