analysis(matlab): rate-variant + vertical x-axis (templates, generators, summary)

Refactor variation_{analyze,power,logpower,plot}.m to run both count and rate
metrics; plot x-axis labels vertical (xtickangle 90). SUMMARY.csv gains
interaction_p_rate / interaction_p_rs_rate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Experiments DB Dev
2026-07-24 02:32:43 -04:00
parent a2a812acc8
commit 1009f263e8
6 changed files with 190 additions and 155 deletions
+63 -39
View File
@@ -1,30 +1,58 @@
% Variation analysis -- the paper's linear mixed model on the successful-reach
% COUNT, fit on this folder's curated data subset.
% Variation analysis -- the paper's linear mixed model on this folder's data,
% for BOTH metrics:
% metric = count : behavior = # successes -> result.txt
% metric = rate : behavior = success / attempts -> result_rate.txt
% (rate uses only sessions with attempts > 0)
%
% model: behavior ~ stim + day + stim:day + (1|rat)
% behavior = successful reaches (count per session)
% stim = 1 for the treatment group(s), 0 for the control group(s)
% day = training day within this window (0 = first analyzed day)
% rat = subject (random intercept)
%
% Self-contained: reads data.csv beside this script and writes result.txt.
% Run headless from this folder with: matlab -batch "analyze"
% (This is a copy of analysis/matlab/variation_analyze.m; see make_variations.m.)
% stim = 1 treatment / 0 control; day = training day within window (0 =
% first analyzed day); rat = subject (random intercept).
% For the interaction we report residual DF, Satterthwaite DF, and the honest
% per-animal random-slope test. Self-contained: reads data.csv beside this
% script. Run headless with: matlab -batch "analyze"
% (Copy of analysis/matlab/variation_analyze.m; see make_variations.m.)
here = fileparts(mfilename('fullpath'));
if isempty(here); here = pwd; end
vname = regexprep(here, '.*[/\\]', ''); % folder name = variation id
vname = regexprep(here, '.*[/\\]', '');
D = readtable(fullfile(here, 'data.csv'), 'TextType', 'string');
tbl = table(D.success, D.day - min(D.day), double(D.stim), categorical(D.subject), ...
Rc = localAnalyze(D, 'count', here, vname);
Rr = localAnalyze(D, 'rate', here, vname);
% Machine-readable handoff for SUMMARY.csv (count drives it; rate appended).
VARRESULT = struct('name', vname, 'nRats', Rc.nRats, 'nObs', Rc.nObs, ...
'interP', Rc.interP, 'interEst', Rc.interEst, ...
'interPsatt', Rc.interPsatt, 'interPrs', Rc.interPrs, ...
'stimP', Rc.stimP, 'dayP', Rc.dayP, 'covEqual', Rc.covEqual, ...
'interPrate', Rr.interP, 'interEstRate', Rr.interEst, 'interPrsRate', Rr.interPrs);
% ------------------------------------------------------------------ helper
function R = localAnalyze(D, metric, here, vname)
if strcmp(metric, 'rate')
D = D(D.total > 0, :);
beh = D.success ./ D.total;
mlabel = 'success RATE (success/attempts)'; suffix = '_rate';
else
beh = D.success;
mlabel = 'success COUNT'; suffix = '';
end
R = struct('interP', NaN, 'interEst', NaN, 'interPsatt', NaN, 'interPrs', NaN, ...
'stimP', NaN, 'dayP', NaN, 'nRats', numel(unique(D.subject)), ...
'nObs', height(D), 'covEqual', false);
if numel(unique(D.stim)) < 2 || numel(unique(D.day)) < 2
localWrite(sprintf('VARIATION: %s [metric: %s]\nInsufficient data for this metric.\n', ...
vname, mlabel), here, suffix);
return
end
tbl = table(beh, 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
As = anova(m, 'DFMethod', 'satterthwaite');
% 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
@@ -43,6 +71,7 @@ row = @(nm, t) sprintf('%-26s t(%d)=%6.2f F(%d)=%7.3f p=%.4g p=%.4g (df=%.0f
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)));
ii = gi('day:stim'); pI = C.pValue(ii); eI = C.Estimate(ii);
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));
if abs(maxT - maxC) > 2
@@ -50,20 +79,14 @@ if abs(maxT - maxC) > 2
else
cov = '(equal day coverage over this window)';
end
ii = gi('day:stim'); pI = C.pValue(ii); eI = C.Estimate(ii);
if pI >= 0.05
verdict = 'n.s. -- slopes parallel (no differential learning rate)';
elseif eI > 0
verdict = 'SIGNIFICANT positive -- treatment improves FASTER (benefit accumulates)';
else
verdict = 'SIGNIFICANT negative -- treatment improves SLOWER (groups converge)';
end
if pI >= 0.05; verdict = 'n.s. -- slopes parallel (no differential learning rate)';
elseif eI > 0; verdict = 'SIGNIFICANT positive -- treatment improves FASTER (benefit accumulates)';
else; verdict = 'SIGNIFICANT negative -- treatment improves SLOWER (groups converge)'; end
bar = repmat('=', 1, 78);
raw = regexprep(evalc('disp(m)'), '</?strong>', '');
s = sprintf('%s\nVARIATION: %s\n%s\n', bar, vname, bar);
s = [s sprintf('model: behavior ~ stim + day + stim:day + (1|rat) (behavior = success COUNT)\n')];
s = sprintf('%s\nVARIATION: %s [metric: %s]\n%s\n', bar, vname, mlabel, bar);
s = [s sprintf('model: behavior ~ stim + day + stim:day + (1|rat) (behavior = %s)\n', mlabel)];
s = [s sprintf('day = training day within window (0 = first analyzed day)\n')];
s = [s sprintf('treatment (stim=1): %s\n', strjoin(cellstr(unique(D.group(D.stim == 1))), ', '))];
s = [s sprintf('control (stim=0): %s\n', strjoin(cellstr(unique(D.group(D.stim == 0))), ', '))];
@@ -74,7 +97,7 @@ s = [s sprintf('%-26s %-18s %-12s %s\n%s\n', 'effect', 't(df) / F(df1)', 'p (res
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))];
s = [s sprintf('interaction 95%% CI: [%+.4g, %+.4g]\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
@@ -82,17 +105,18 @@ else
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')];
s = [s sprintf('INTERPRETATION: stim x day interaction %s (p=%.4g, slope diff=%+.4g)\n', verdict, pI, eI)];
s = [s sprintf('Paper (N=24, count): interaction t(227)=2.68, F(1)=7.12, p=0.008.\n')];
localWrite(s, here, suffix);
R = struct('interP', pI, 'interEst', eI, 'interPsatt', As.pValue(gs('day:stim')), ...
'interPrs', rsP, 'stimP', C.pValue(gi('stim')), 'dayP', C.pValue(gi('day')), ...
'nRats', numel(unique(D.subject)), 'nObs', height(D), 'covEqual', abs(maxT - maxC) <= 2);
end
function localWrite(s, here, suffix)
fprintf('%s', s);
fid = fopen(fullfile(here, 'result.txt'), 'w');
fprintf(fid, '%s', s);
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);
fid = fopen(fullfile(here, ['result' suffix '.txt']), 'w');
fprintf(fid, '%s', s); fclose(fid);
end