function T = tdcs_load_data() %TDCS_LOAD_DATA Load, join, and curate the raw tDCS reaching matrices. % T = TDCS_LOAD_DATA() reads analysis/matlab/raw/raw_success.csv and % raw_total.csv (app-exported matrix format: metric header line, blank % line, file "Group" line, "# Days Reach" subject-header line, then one % row per day with one value per subject column), reshapes each to long % form, joins them on (subject, day), applies the curated 12-animal % group map from tdcs_config (the file's own Group row is ignored), % and restricts to day >= 0. % % Returns a table with variables: % subject (string) animal name % group (categorical) curated group (Naive, Electrode-Box-A, % Electrode-Box-A2, Electrode-Box-B2, % Right-Electrode) % day (double) "# Days Reach" value, >= 0 % success (double) successful reaches in the session % total (double) total attempts in the session % % Only the 12 curated animals (per tdcs_config().groups) are retained; % any other animal present in the raw export is dropped. thisDir = fileparts(mfilename('fullpath')); successFile = fullfile(thisDir, 'raw', 'raw_success.csv'); totalFile = fullfile(thisDir, 'raw', 'raw_total.csv'); Tsuccess = localReadMatrix(successFile, 'success'); Ttotal = localReadMatrix(totalFile, 'total'); % Left join keyed on (subject, day): every non-blank Success cell becomes % a row; Total is looked up for the same (subject, day). Per the plan's % Global Constraints, Total is fully populated wherever Success is, so % this should never introduce a missing total — but the join is a left % join (not an inner join) specifically so that an unmatched Success row % would still be kept (with total = NaN) rather than silently dropped. T = outerjoin(Tsuccess, Ttotal, 'Keys', {'subject', 'day'}, ... 'MergeKeys', true, 'Type', 'left'); % Apply the curated map: keep only the 12 curated animals, and assign % group from the map (overriding the file's own Group row). cfg = tdcs_config(); subjectsCell = cellstr(T.subject); keep = isKey(cfg.groups, subjectsCell); T = T(keep, :); subjectsCell = subjectsCell(keep); groupVals = cell(height(T), 1); for i = 1:height(T) groupVals{i} = cfg.groups(subjectsCell{i}); end T.group = categorical(groupVals); % Restrict to day >= 0 (analysis window). T = T(T.day >= 0, :); % Final variable order and a deterministic row order. T = T(:, {'subject', 'group', 'day', 'success', 'total'}); T = sortrows(T, {'subject', 'day'}); end function Tlong = localReadMatrix(filePath, valueName) %LOCALREADMATRIX Parse one app-exported day-by-subject matrix to long form. % Line 1 = "Data," (skipped), line 2 blank (skipped), line 3 = % "Group,<...>" (skipped -- grouping comes from the curated map, not % this row), line 4 = "# Days Reach," (columns 2:end % are the subject names), lines 5+ = "," with % blank cells meaning "no session that day for that subject". lines = readlines(filePath); % Defensive: drop any wholly-blank trailing line (e.g. if the file ends % with a newline). Real data rows always start with a day number and are % never zero-length. while numel(lines) > 0 && strlength(strip(lines(end))) == 0 lines(end) = []; end headerParts = strsplit(lines(4), ',', 'CollapseDelimiters', false); subjects = string(headerParts(2:end)); nSubjects = numel(subjects); dataLines = lines(5:end); dataLines = dataLines(strlength(strip(dataLines)) > 0); nRows = numel(dataLines); day = nan(nRows * nSubjects, 1); subjectCol = strings(nRows * nSubjects, 1); value = nan(nRows * nSubjects, 1); idx = 0; for r = 1:nRows parts = strsplit(dataLines(r), ',', 'CollapseDelimiters', false); dayVal = str2double(parts(1)); for c = 1:nSubjects col = c + 1; if col <= numel(parts) cellStr = strtrim(parts(col)); else cellStr = ""; end if strlength(cellStr) == 0 continue end idx = idx + 1; day(idx) = dayVal; subjectCol(idx) = subjects(c); value(idx) = str2double(cellStr); end end day = day(1:idx); subjectCol = subjectCol(1:idx); value = value(1:idx); Tlong = table(subjectCol, day, value, 'VariableNames', {'subject', 'day', valueName}); end