fix(matlab): report learning rate via per-animal slope test, not anticonservative GLMM F-test

fitglme coefTest only offers observation-level DF (df2~=178), which overstates
the group x day interaction significance for this few-subject design (p~=0 in
every scenario). Replace the reported learning-rate result with a cluster-honest
per-animal slope test (per-subject OLS slope of success vs day, Welch + MWU,
B2 vs A2) -- now correctly parallel in all 6 scenarios, matching the Python GEE.
The GLMM F-test is kept as a flagged reference only. Adds a regression test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Experiments DB Dev
2026-07-20 08:40:47 -04:00
parent 13414d31fc
commit b1057f6105
3 changed files with 70 additions and 9 deletions
+13 -5
View File
@@ -87,11 +87,19 @@ s = [s localCleanDisp(R.countGLME) sprintf('\n')];
s = [s localHeader('(B) LEVEL / RATE -- Binomial GLMM (subject random intercept)')];
s = [s localCleanDisp(R.rateGLME) sprintf('\n')];
s = [s localHeader('(C) LEARNING RATE -- joint test (all group x day_c interactions = 0)')];
s = [s sprintf('F=%.3f, df1=%d, df2=%.2f, p=%.4f\n', ...
R.learn.jointF, R.learn.jointDF1, R.learn.jointDF2, R.learn.jointP)];
s = [s sprintf([' -> small p = groups improve at DIFFERENT rates; ' ...
'large p = parallel learning.\n\n'])];
s = [s localHeader('(C) LEARNING RATE -- per-animal slope test (Box-B2 vs Box-A2)')];
if R.learn.slopeMWUp < 0.05
verdict = 'DIFFERENT learning rates';
else
verdict = 'parallel learning (no slope difference detected)';
end
s = [s sprintf(['Per-subject OLS slope of success vs day: Box-B2 mean=%.2f, Box-A2 mean=%.2f\n' ...
' Welch two-sided p=%.3f, Mann-Whitney p=%.3f (nB=%d, nA=%d) -> %s\n'], ...
R.learn.meanSlopeB, R.learn.meanSlopeA, R.learn.slopeWelchP, R.learn.slopeMWUp, ...
R.learn.nB, R.learn.nA, verdict)];
s = [s sprintf(['(Reference only: the GLMM group x day_c joint F-test gives p=%.4f, but with just\n' ...
' observation-level DF (df2=%.0f) it is ANTICONSERVATIVE for this few-subject design and is\n' ...
' NOT the basis for the conclusion above.)\n\n'], R.learn.glmeJointP, R.learn.glmeJointDF2)];
end
function s = localCleanDisp(model)