From d9b65a3279672d077b844c5bb7792eb1507ffb3f Mon Sep 17 00:00:00 2001 From: Experiments DB Dev Date: Wed, 22 Jul 2026 14:18:47 -0400 Subject: [PATCH] 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 --- analysis/forouzan_reach_data.csv | 232 +++++++++++++++++++++++++++ analysis/matlab/forouzan_load_data.m | 78 +++++++++ analysis/matlab/make_forouzan_data.m | 38 +++++ 3 files changed, 348 insertions(+) create mode 100644 analysis/forouzan_reach_data.csv create mode 100644 analysis/matlab/forouzan_load_data.m create mode 100644 analysis/matlab/make_forouzan_data.m diff --git a/analysis/forouzan_reach_data.csv b/analysis/forouzan_reach_data.csv new file mode 100644 index 0000000..86c6fe8 --- /dev/null +++ b/analysis/forouzan_reach_data.csv @@ -0,0 +1,232 @@ +subject,group,day,success,total +Afrasyab,a2_f,0,1,13 +Afrasyab,a2_f,1,8,24 +Afrasyab,a2_f,2,13,35 +Afrasyab,a2_f,3,8,29 +Afrasyab,a2_f,4,21,43 +Afrasyab,a2_f,5,14,42 +Afrasyab,a2_f,6,18,46 +Afrasyab,a2_f,7,21,52 +Afrasyab,a2_f,8,21,41 +Afrasyab,a2_f,9,21,55 +Arash,b2_f,0,9,34 +Arash,b2_f,1,12,41 +Arash,b2_f,2,15,43 +Arash,b2_f,3,18,44 +Arash,b2_f,4,16,33 +Arash,b2_f,5,18,47 +Arash,b2_f,6,13,48 +Arash,b2_f,7,9,38 +Arash,b2_f,8,18,43 +Arash,b2_f,9,15,47 +Ashkas,a2_f,0,2,30 +Ashkas,a2_f,1,11,35 +Ashkas,a2_f,2,10,30 +Ashkas,a2_f,3,9,33 +Ashkas,a2_f,4,9,41 +Ashkas,a2_f,5,10,36 +Ashkas,a2_f,6,13,46 +Ashkas,a2_f,7,9,42 +Ashkas,a2_f,8,6,36 +Ashkas,a2_f,9,15,48 +Fariborz,a2_f,0,8,32 +Fariborz,a2_f,1,10,32 +Fariborz,a2_f,2,9,34 +Fariborz,a2_f,3,11,35 +Fariborz,a2_f,4,13,35 +Fariborz,a2_f,5,5,38 +Fariborz,a2_f,6,12,44 +Garsivaz,b2_f,0,10,34 +Garsivaz,b2_f,1,12,30 +Garsivaz,b2_f,2,14,35 +Garsivaz,b2_f,3,27,56 +Garsivaz,b2_f,4,22,41 +Garsivaz,b2_f,5,30,57 +Garsivaz,b2_f,6,34,52 +Garsivaz,b2_f,7,25,49 +Garsivaz,b2_f,8,29,46 +Garsivaz,b2_f,9,25,44 +Iraj,b2_f,0,6,29 +Iraj,b2_f,1,13,37 +Iraj,b2_f,2,15,32 +Iraj,b2_f,3,20,49 +Iraj,b2_f,4,17,47 +Iraj,b2_f,5,21,47 +Iraj,b2_f,6,24,49 +Khosrow,b2_f,0,6,23 +Khosrow,b2_f,1,7,27 +Khosrow,b2_f,2,10,27 +Khosrow,b2_f,3,10,26 +Khosrow,b2_f,4,14,35 +Khosrow,b2_f,5,20,41 +Khosrow,b2_f,6,11,37 +Khosrow,b2_f,7,13,34 +Khosrow,b2_f,8,13,30 +Khosrow,b2_f,9,15,40 +Kiyanoush,b2_f,0,11,19 +Kiyanoush,b2_f,1,22,52 +Kiyanoush,b2_f,2,34,58 +Kiyanoush,b2_f,3,30,52 +Kiyanoush,b2_f,4,27,56 +Kiyanoush,b2_f,5,46,65 +Kiyanoush,b2_f,6,32,58 +Kiyanoush,b2_f,7,47,61 +Kiyanoush,b2_f,8,49,68 +Kiyanoush,b2_f,9,41,49 +Manuchehr,b2_f,0,1,22 +Manuchehr,b2_f,1,8,18 +Manuchehr,b2_f,2,9,41 +Manuchehr,b2_f,3,14,47 +Manuchehr,b2_f,4,25,60 +Manuchehr,b2_f,5,22,70 +Manuchehr,b2_f,6,37,74 +Manuchehr,b2_f,7,40,75 +Manuchehr,b2_f,8,32,55 +Manuchehr,b2_f,9,29,69 +Mehrab,a2_f,0,8,28 +Mehrab,a2_f,1,13,26 +Mehrab,a2_f,2,16,45 +Mehrab,a2_f,3,17,34 +Mehrab,a2_f,4,21,45 +Mehrab,a2_f,5,21,55 +Mehrab,a2_f,6,22,51 +Mehrab,a2_f,7,21,48 +Mehrab,a2_f,8,21,48 +Mehrab,a2_f,9,23,45 +Merdas,a2_f,0,7,30 +Merdas,a2_f,1,9,36 +Merdas,a2_f,2,8,33 +Merdas,a2_f,3,17,48 +Merdas,a2_f,4,21,45 +Merdas,a2_f,5,13,39 +Merdas,a2_f,6,19,43 +Merdas,a2_f,7,10,48 +Merdas,a2_f,8,19,52 +Merdas,a2_f,9,20,47 +Nozar,a2_f,0,4,20 +Nozar,a2_f,1,5,23 +Nozar,a2_f,2,8,43 +Nozar,a2_f,3,8,48 +Nozar,a2_f,4,11,40 +Nozar,a2_f,5,13,53 +Nozar,a2_f,6,15,50 +Nozar,a2_f,7,19,46 +Nozar,a2_f,8,18,56 +Nozar,a2_f,9,20,47 +Pashang,b2_f,0,5,32 +Pashang,b2_f,1,7,51 +Pashang,b2_f,2,18,49 +Pashang,b2_f,3,16,43 +Pashang,b2_f,4,21,36 +Pashang,b2_f,5,16,53 +Pashang,b2_f,6,18,42 +Pashang,b2_f,7,23,44 +Pashang,b2_f,8,20,43 +Pashang,b2_f,9,34,57 +Pashin,a2_f,0,7,27 +Pashin,a2_f,1,9,22 +Pashin,a2_f,2,11,41 +Pashin,a2_f,3,14,40 +Pashin,a2_f,4,12,36 +Pashin,a2_f,5,14,43 +Pashin,a2_f,6,11,42 +Pashin,a2_f,7,14,47 +Pashin,a2_f,8,13,50 +Pashin,a2_f,9,18,45 +Rostam,b2_f,0,9,29 +Rostam,b2_f,1,12,36 +Rostam,b2_f,2,14,42 +Rostam,b2_f,3,18,41 +Rostam,b2_f,4,15,45 +Rostam,b2_f,5,20,49 +Rostam,b2_f,6,21,51 +Rostam,b2_f,7,24,48 +Rostam,b2_f,8,19,46 +Rostam,b2_f,9,23,48 +Salm,a2_f,0,7,28 +Salm,a2_f,1,10,36 +Salm,a2_f,2,16,50 +Salm,a2_f,3,18,49 +Salm,a2_f,4,21,62 +Salm,a2_f,5,26,60 +Salm,a2_f,6,27,53 +Salm,a2_f,7,32,62 +Salm,a2_f,8,16,40 +Salm,a2_f,9,16,43 +Sam,b2_f,0,8,28 +Sam,b2_f,1,11,32 +Sam,b2_f,2,10,26 +Sam,b2_f,3,11,39 +Sam,b2_f,4,12,36 +Sam,b2_f,5,14,34 +Sam,b2_f,6,15,43 +Sam,b2_f,7,9,31 +Sam,b2_f,8,23,51 +Sam,b2_f,9,19,45 +Siavash,a2_f,0,10,21 +Siavash,a2_f,1,3,25 +Siavash,a2_f,2,7,39 +Siavash,a2_f,3,13,36 +Siavash,a2_f,4,12,35 +Siavash,a2_f,5,18,32 +Siavash,a2_f,6,18,45 +Siavash,a2_f,7,22,37 +Siavash,a2_f,8,18,36 +Siavash,a2_f,9,17,34 +Sohrab,b2_f,0,8,23 +Sohrab,b2_f,1,11,34 +Sohrab,b2_f,2,15,45 +Sohrab,b2_f,3,19,47 +Sohrab,b2_f,4,18,50 +Sohrab,b2_f,5,18,45 +Sohrab,b2_f,6,16,45 +Sohrab,b2_f,7,20,49 +Sohrab,b2_f,8,14,32 +Sohrab,b2_f,9,23,43 +Tahmasb,b2_f,0,7,32 +Tahmasb,b2_f,1,11,38 +Tahmasb,b2_f,2,8,37 +Tahmasb,b2_f,3,7,26 +Tahmasb,b2_f,4,12,35 +Tahmasb,b2_f,5,13,39 +Tahmasb,b2_f,6,11,41 +Tahmasb,b2_f,7,15,52 +Tahmasb,b2_f,8,23,61 +Tahmasb,b2_f,9,22,61 +Tur,a2_f,0,6,39 +Tur,a2_f,1,11,36 +Tur,a2_f,2,9,35 +Tur,a2_f,3,16,45 +Tur,a2_f,4,19,52 +Tur,a2_f,5,14,47 +Tur,a2_f,6,21,46 +Tus,b2_f,0,4,32 +Tus,b2_f,1,9,39 +Tus,b2_f,2,5,40 +Tus,b2_f,3,15,42 +Tus,b2_f,4,21,50 +Tus,b2_f,5,16,52 +Tus,b2_f,6,19,59 +Tus,b2_f,7,28,57 +Tus,b2_f,8,27,62 +Tus,b2_f,9,27,60 +Zal,a2_f,0,5,34 +Zal,a2_f,1,12,36 +Zal,a2_f,2,16,37 +Zal,a2_f,3,18,40 +Zal,a2_f,4,14,34 +Zal,a2_f,5,15,41 +Zal,a2_f,6,16,46 +Zal,a2_f,7,15,44 +Zal,a2_f,8,20,46 +Zal,a2_f,9,18,50 +Zav,a2_f,0,4,28 +Zav,a2_f,1,7,37 +Zav,a2_f,2,7,41 +Zav,a2_f,3,4,32 +Zav,a2_f,4,8,37 +Zav,a2_f,5,4,37 +Zav,a2_f,6,7,36 +Zav,a2_f,7,15,56 +Zav,a2_f,8,19,54 +Zav,a2_f,9,21,49 diff --git a/analysis/matlab/forouzan_load_data.m b/analysis/matlab/forouzan_load_data.m new file mode 100644 index 0000000..f468bba --- /dev/null +++ b/analysis/matlab/forouzan_load_data.m @@ -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 + day(end + 1, 1) = str2double(p(2)) - 1; %#ok Session 1 -> day 0 + success(end + 1, 1) = suc; %#ok + total(end + 1, 1) = tot; %#ok +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 diff --git a/analysis/matlab/make_forouzan_data.m b/analysis/matlab/make_forouzan_data.m new file mode 100644 index 0000000..ad70b7c --- /dev/null +++ b/analysis/matlab/make_forouzan_data.m @@ -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