analysis(matlab): convert previous (Forouzan) study to our long format

Add forouzan_load_data.m (joins raw/Data_Forouzan.csv + raw/Total_Success.csv)
and make_forouzan_data.m, producing analysis/forouzan_reach_data.csv in our
curated column format (subject, group, day, success, total). Group is mapped
Anodal -> b2_f (tDCS), Control -> a2_f (control); Hand ignored; day 0-indexed
(Session 1 = day 0 = paper 'Day 1'); early-stopped sessions (total=0) dropped.

Conversion validated by refitting the paper's own LME on the converted data:
interaction t(227)=2.66, F(1)=7.09, p=0.0083 -- reproduces the paper's
reported t(227)=2.68, F(1)=7.12, p=0.008, N=24 (12 Anodal / 12 Control).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Experiments DB Dev
2026-07-22 14:18:47 -04:00
parent a4dff31772
commit d9b65a3279
3 changed files with 348 additions and 0 deletions
+78
View File
@@ -0,0 +1,78 @@
function T = forouzan_load_data()
%FOROUZAN_LOAD_DATA Load the previous (Forouzan) tDCS reaching study into our
% long format, with group labels matching our convention.
%
% T = FOROUZAN_LOAD_DATA() reads two raw files under analysis/matlab/raw/:
% Data_Forouzan.csv long, fill-down RatName; per rat/session: success,
% failure, total (attempts). Sessions 1..10; rats that
% stopped early have blank success and total = 0
% (encoded #DIV/0!), which are dropped.
% Total_Success.csv per-rat Group (Anodal | Control) and Hand.
%
% and returns a table with the same variables as TDCS_LOAD_DATA:
% subject (string) rat name
% group (categorical) 'b2_f' for Anodal (tDCS), 'a2_f' for Control
% day (double) Session - 1, so day 0 = the paper's "Day 1"
% (matching our 0-indexed day convention)
% success (double) successful reaches in the session
% total (double) total attempts in the session
%
% Hand is ignored per request. The "_f" suffix marks this as the Forouzan
% (previous) dataset, distinct from our Electrode-Box-A2/B2 animals.
thisDir = fileparts(mfilename('fullpath'));
longFile = fullfile(thisDir, 'raw', 'Data_Forouzan.csv');
grpFile = fullfile(thisDir, 'raw', 'Total_Success.csv');
% --- group map: rat -> Anodal|Control -> b2_f|a2_f -----------------------
G = readtable(grpFile, 'VariableNamingRule', 'preserve');
ratNames = string(G{:, 1}); % col 1 = Rat_Name (BOM-safe by index)
rawGroup = string(G{:, end}); % last col = Group (Anodal|Control)
% Keep only the real Anodal/Control rows (drops any header/blank artifact that
% readtable mis-parses as data because of the numeric 0..9 column headers).
keep = ismember(rawGroup, ["Anodal", "Control"]);
ratNames = ratNames(keep);
rawGroup = rawGroup(keep);
label = repmat("a2_f", numel(ratNames), 1);
label(rawGroup == "Anodal") = "b2_f"; % Control -> a2_f (default), Anodal -> b2_f
grpMap = containers.Map(cellstr(ratNames), cellstr(label));
% --- parse the long success/total file (fill-down RatName) ---------------
lines = readlines(longFile);
lines = lines(strlength(strip(lines)) > 0);
lines(1) = erase(lines(1), char(65279)); % strip UTF-8 BOM from the header
subj = strings(0, 1); day = []; success = []; total = [];
curRat = "";
for i = 2:numel(lines) % skip header row
p = strsplit(lines(i), ',', 'CollapseDelimiters', false);
name = strtrim(p(1));
if strlength(name) > 0
curRat = name;
end
suc = str2double(p(3));
tot = str2double(p(5));
if isnan(suc) || isnan(tot) || tot == 0 % no session that day -> drop
continue
end
subj(end + 1, 1) = curRat; %#ok<AGROW>
day(end + 1, 1) = str2double(p(2)) - 1; %#ok<AGROW> Session 1 -> day 0
success(end + 1, 1) = suc; %#ok<AGROW>
total(end + 1, 1) = tot; %#ok<AGROW>
end
% --- attach group and finalise -------------------------------------------
grp = strings(numel(subj), 1);
for i = 1:numel(subj)
if ~isKey(grpMap, char(subj(i)))
error('forouzan_load_data:missingRat', ...
'Rat "%s" in Data_Forouzan.csv has no group in Total_Success.csv.', subj(i));
end
grp(i) = grpMap(char(subj(i)));
end
T = table(subj, categorical(grp), day, success, total, ...
'VariableNames', {'subject', 'group', 'day', 'success', 'total'});
T = sortrows(T, {'subject', 'day'});
end
+38
View File
@@ -0,0 +1,38 @@
function make_forouzan_data()
%MAKE_FOROUZAN_DATA Convert the previous (Forouzan) study to our long format.
% MAKE_FOROUZAN_DATA() calls FOROUZAN_LOAD_DATA and writes the curated long
% table to analysis/forouzan_reach_data.csv (same columns as our
% analysis/tdcs_reach_data.csv: subject, group, day, success, total), with
% group in {a2_f (Control), b2_f (Anodal)}. It then prints a descriptives
% summary and, as a conversion sanity-check, fits the paper's LME
% (behavior ~ stim + day + stim:day + (1|rat)) and compares the interaction
% to the paper's reported values (t(227)=2.68, F(1)=7.12, p=0.008, N=24).
thisDir = fileparts(mfilename('fullpath'));
T = forouzan_load_data();
outFile = fullfile(fileparts(thisDir), 'forouzan_reach_data.csv');
writetable(T, outFile);
fprintf('Wrote %s (%d rows)\n\n', outFile, height(T));
fprintf('%-6s %6s %10s %12s %10s %8s\n', ...
'group', 'nRats', 'nSessions', 'mean_success', 'mean_rate', 'max_day');
for g = categories(T.group)'
r = T.group == g{1};
fprintf('%-6s %6d %10d %12.1f %10.3f %8d\n', g{1}, ...
numel(unique(T.subject(r))), sum(r), mean(T.success(r)), ...
sum(T.success(r)) / sum(T.total(r)), max(T.day(r)));
end
% --- conversion sanity-check: the paper's own LME on the converted data ---
tbl = table(T.success, T.day, double(T.group == 'b2_f'), categorical(T.subject), ...
'VariableNames', {'behavior', 'day', 'stim', 'rat'});
m = fitlme(tbl, 'behavior ~ stim + day + stim:day + (1|rat)');
C = m.Coefficients; A = anova(m);
ii = strcmp(C.Name, 'day:stim'); ai = strcmp(A.Term, 'day:stim');
fprintf(['\nPaper-LME check (behavior ~ stim + day + stim:day + (1|rat), N=%d):\n' ...
' stim x day interaction: t(%d)=%.2f, F(1)=%.2f, p=%.4g\n' ...
' paper reported: t(227)=2.68, F(1)=7.12, p=0.008\n'], ...
numel(unique(tbl.rat)), C.DF(ii), C.tStat(ii), A.FStat(ai), C.pValue(ii));
end