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>
This commit is contained in:
@@ -21,11 +21,27 @@ tbl = table(D.success, D.day - min(D.day), double(D.stim), categorical(D.subject
|
||||
'VariableNames', {'behavior', 'day', 'stim', 'rat'});
|
||||
m = fitlme(tbl, 'behavior ~ stim + day + stim:day + (1|rat)');
|
||||
C = m.Coefficients; A = anova(m); ci = coefCI(m);
|
||||
As = anova(m, 'DFMethod', 'satterthwaite'); % Satterthwaite denominator DF
|
||||
|
||||
% Honest test: refit with a per-animal random SLOPE so the interaction DF
|
||||
% collapses toward the animal count (guarded -- may not converge in short windows).
|
||||
rsP = NaN; rsDf = NaN; rsF = NaN; rsOk = false;
|
||||
wst = warning('off', 'all');
|
||||
try
|
||||
mr = fitlme(tbl, 'behavior ~ stim + day + stim:day + (day|rat)');
|
||||
Ar = anova(mr, 'DFMethod', 'satterthwaite');
|
||||
ri = strcmp(Ar.Term, 'day:stim');
|
||||
rsF = Ar.FStat(ri); rsDf = Ar.DF2(ri); rsP = Ar.pValue(ri); rsOk = true;
|
||||
catch
|
||||
end
|
||||
warning(wst);
|
||||
|
||||
gi = @(t) find(strcmp(C.Name, t), 1);
|
||||
ga = @(t) find(strcmp(A.Term, t), 1);
|
||||
row = @(nm, t) sprintf('%-26s t(%d)=%6.2f F(%d)=%8.3f p=%.4g\n', nm, ...
|
||||
C.DF(gi(t)), C.tStat(gi(t)), A.DF1(ga(t)), A.FStat(ga(t)), C.pValue(gi(t)));
|
||||
gs = @(t) find(strcmp(As.Term, t), 1);
|
||||
row = @(nm, t) sprintf('%-26s t(%d)=%6.2f F(%d)=%7.3f p=%.4g p=%.4g (df=%.0f)\n', nm, ...
|
||||
C.DF(gi(t)), C.tStat(gi(t)), A.DF1(ga(t)), A.FStat(ga(t)), C.pValue(gi(t)), ...
|
||||
As.pValue(gs(t)), As.DF2(gs(t)));
|
||||
|
||||
maxT = max(D.day(D.stim == 1)); minT = min(D.day(D.stim == 1));
|
||||
maxC = max(D.day(D.stim == 0)); minC = min(D.day(D.stim == 0));
|
||||
@@ -54,11 +70,18 @@ s = [s sprintf('control (stim=0): %s\n', strjoin(cellstr(unique(D.group(D.stim
|
||||
s = [s sprintf('N = %d rats, %d sessions raw day coverage: treat %d..%d, control %d..%d\n', ...
|
||||
numel(unique(D.subject)), height(D), minT, maxT, minC, maxC)];
|
||||
s = [s sprintf('%s\n\n%s\nFULL MODEL SUMMARY -- fitlme\n%s\n%s\n', cov, bar, bar, raw)];
|
||||
s = [s sprintf('%-26s %-13s %-13s %s\n%s\n', 'effect', 't (df)', 'F (df1)', 'p', repmat('-', 1, 66))];
|
||||
s = [s sprintf('%-26s %-18s %-12s %s\n%s\n', 'effect', 't(df) / F(df1)', 'p (resid)', 'Satterthwaite: p (df)', repmat('-', 1, 76))];
|
||||
s = [s row('stim x day (interaction)', 'day:stim')];
|
||||
s = [s row('day (learning)', 'day')];
|
||||
s = [s row('stim (main, window start)', 'stim')];
|
||||
s = [s sprintf('interaction 95%% CI: [%+.2f, %+.2f]\n', ci(ii, 1), ci(ii, 2))];
|
||||
if rsOk
|
||||
s = [s sprintf('HONEST LME (per-animal random slope, day|rat): interaction F(1,%.1f)=%.2f, p=%.4g\n', rsDf, rsF, rsP)];
|
||||
else
|
||||
s = [s sprintf('HONEST LME (per-animal random slope, day|rat): did not converge for this window.\n')];
|
||||
end
|
||||
s = [s sprintf([' (Satterthwaite DF ~= residual on this random-intercept model; the random-slope\n' ...
|
||||
' model above is the honest learning-rate test -- DF collapses toward the animal count.)\n'])];
|
||||
s = [s sprintf('INTERPRETATION: stim x day interaction %s (p=%.4g, slope diff=%+.2f)\n', verdict, pI, eI)];
|
||||
s = [s sprintf('Paper (N=24): interaction t(227)=2.68, F(1)=7.12, p=0.008.\n')];
|
||||
|
||||
@@ -70,5 +93,6 @@ fclose(fid);
|
||||
% Machine-readable handoff for the summary table (see make_variations.m).
|
||||
VARRESULT = struct('name', vname, 'nRats', numel(unique(D.subject)), ...
|
||||
'nObs', height(D), 'interP', pI, 'interEst', eI, ...
|
||||
'interPsatt', As.pValue(gs('day:stim')), 'interPrs', rsP, ...
|
||||
'stimP', C.pValue(gi('stim')), 'dayP', C.pValue(gi('day')), ...
|
||||
'covEqual', abs(maxT - maxC) <= 2);
|
||||
|
||||
@@ -55,11 +55,14 @@ Group: Error
|
||||
{'Res Std'} 14.12 12.139 16.425
|
||||
|
||||
|
||||
effect t (df) F (df1) p
|
||||
------------------------------------------------------------------
|
||||
stim x day (interaction) t(80)= 2.01 F(1)= 4.031 p=0.04804
|
||||
day (learning) t(80)= 9.45 F(1)= 89.224 p=1.171e-14
|
||||
stim (main, window start) t(80)= 0.63 F(1)= 0.401 p=0.5285
|
||||
effect t(df) / F(df1) p (resid) Satterthwaite: p (df)
|
||||
----------------------------------------------------------------------------
|
||||
stim x day (interaction) t(80)= 2.01 F(1)= 4.031 p=0.04804 p=0.04788 (df=84)
|
||||
day (learning) t(80)= 9.45 F(1)= 89.224 p=1.171e-14 p=7.518e-15 (df=84)
|
||||
stim (main, window start) t(80)= 0.63 F(1)= 0.401 p=0.5285 p=0.5284 (df=84)
|
||||
interaction 95% CI: [+0.02, +3.39]
|
||||
HONEST LME (per-animal random slope, day|rat): interaction F(1,20.2)=2.92, p=0.1029
|
||||
(Satterthwaite DF ~= residual on this random-intercept model; the random-slope
|
||||
model above is the honest learning-rate test -- DF collapses toward the animal count.)
|
||||
INTERPRETATION: stim x day interaction SIGNIFICANT positive -- treatment improves FASTER (benefit accumulates) (p=0.04804, slope diff=+1.70)
|
||||
Paper (N=24): interaction t(227)=2.68, F(1)=7.12, p=0.008.
|
||||
|
||||
Reference in New Issue
Block a user