function tdcs_lme_report(L, scenario, cfg) %TDCS_LME_REPORT Print + save the days x tDCS linear-mixed-model report. % TDCS_LME_REPORT(L, SCENARIO, CFG) formats the result of TDCS_LME to the % console and to analysis/matlab/results/.txt, in the style of the % published result (interaction, days, tDCS effects) plus interpretation and % the paper's reference numbers. bar = repmat('=', 1, 78); s = sprintf('%s\n', bar); s = [s sprintf('LINEAR MIXED MODEL (days x tDCS) -- scenario: %s\n', scenario)]; s = [s sprintf('%s\n', bar)]; s = [s sprintf('model: success ~ day * tDCS + (1|subject) [tDCS: %s = 1 vs %s = 0]\n', ... cfg.anchorHigh, cfg.anchorLow)]; s = [s sprintf('N = %d subjects, %d sessions\n', L.nSubjects, L.nObs)]; s = [s sprintf('day coverage: Box-B2 0..%d, Box-A2 0..%d\n', L.maxDayB, L.maxDayA)]; s = [s sprintf('(day is raw and 0-indexed: our day 0 = the paper''s "Day 1", so the tDCS\n')]; s = [s sprintf(' main effect below is the group difference on Day 1 -- comparable to the paper.)\n')]; if abs(L.maxDayB - L.maxDayA) > 2 s = [s sprintf(['** WARNING: the groups'' day coverage is UNEQUAL (differ by %d days). The\n' ... ' interaction/slope over this window EXTRAPOLATES the shorter group''s line and is\n' ... ' CONFOUNDED -- prefer the _d0_%d window (both groups have data throughout). **\n'], ... abs(L.maxDayB - L.maxDayA), min(L.maxDayB, L.maxDayA))]; end s = [s sprintf('\n')]; s = [s tdcs_model_summary(L.lme, 'fitlme: success ~ day*tDCS + (1|subject)') sprintf('\n')]; s = [s sprintf('%-27s %-20s %-12s %s\n', 'effect', 't(df) / F(df1)', 'p (resid)', 'Satterthwaite: p (df)')]; s = [s sprintf('%s\n', repmat('-', 1, 78))]; s = [s localRow('days x tDCS (interaction)', L.interaction)]; s = [s localRow('days (learning)', L.day)]; s = [s localRow('tDCS (main, at Day 1)', L.tDCS)]; s = [s localHonest(L.interRS)]; s = [s sprintf('\nINTERPRETATION\n')]; if L.interaction.p >= 0.05 interTxt = 'slopes are parallel (no differential change over training)'; elseif L.interaction.estimate > 0 interTxt = 'the tDCS (Box-B2) group improves FASTER -- benefit ACCUMULATES over training'; else interTxt = 'the tDCS (Box-B2) group improves SLOWER -- groups CONVERGE (Box-B2 is ahead early, the gap narrows)'; end s = [s sprintf(' - days x tDCS interaction: %s (p=%.4f, slope diff=%.2f) -> %s.\n', ... localSig(L.interaction.p), L.interaction.p, L.interaction.estimate, interTxt)]; s = [s sprintf(' - days (learning): %s (p=%.2g) -> performance improves with training.\n', ... localSig(L.day.p), L.day.p)]; s = [s sprintf(' - tDCS main effect on Day 1 (our day 0): %s (p=%.4f) -> the groups %s on Day 1.\n', ... localSig(L.tDCS.p), L.tDCS.p, ... localPick(L.tDCS.p < 0.05, 'already DIFFER', 'are comparable (as in the paper)'))]; s = [s sprintf('\nPaper reference (N=24): interaction t(227)=2.68, F(1)=7.12, p=0.008;\n')]; s = [s sprintf(' days t(227)=9.64, F(1)=267.64, p=1.2e-18; tDCS t(227)=0.23, F(1)=0.053, p=0.81.\n')]; s = [s sprintf(' (Our N and exact statistics differ; this replicates the MODEL FORM on our data.)\n')]; fprintf('%s', s); thisDir = fileparts(mfilename('fullpath')); resDir = fullfile(thisDir, 'results'); if ~exist(resDir, 'dir'); mkdir(resDir); end fid = fopen(fullfile(resDir, [scenario '.txt']), 'w'); if fid < 0 error('tdcs_lme_report:fopen', 'Cannot open results file for "%s".', scenario); end cleanup = onCleanup(@() fclose(fid)); %#ok fprintf(fid, '%s', s); end function r = localRow(name, e) r = sprintf('%-27s t(%d)=%6.2f F(%d)=%7.3f p=%.4g p=%.4g (df=%.0f)\n', ... name, e.df, e.t, e.df1, e.F, e.p, e.pSatt, e.dfSatt); end function s = localHonest(rs) %LOCALHONEST Report the random-slope interaction (the honest LME test) + why % Satterthwaite on the random-intercept model above barely changes the DF. if rs.ok s = sprintf(['\nHONEST LME -- per-animal random slope (day|subject): ' ... 'interaction F(%d,%.1f)=%.2f, p=%.4g\n'], rs.df1, rs.df2, rs.F, rs.p); else s = sprintf(['\nHONEST LME -- per-animal random slope (day|subject): ' ... 'model did not converge for this window.\n']); end s = [s sprintf([' Note: Satterthwaite DF on the random-INTERCEPT model above stays ~= residual\n' ... ' (the slope''s error is at the session level), so it does NOT fix pseudoreplication.\n' ... ' Letting each animal have its OWN slope collapses the interaction DF toward the animal\n' ... ' count -- this, and the per-animal slope test, are the honest learning-rate inference.\n'])]; end function t = localSig(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