matlab(tdcs): data loading + curated group map

This commit is contained in:
Experiments DB Dev
2026-07-20 07:29:31 -04:00
parent 808040a522
commit 33121e3a8f
5 changed files with 316 additions and 0 deletions
+35
View File
@@ -0,0 +1,35 @@
Data,Success
Group,,,Electrode-Box-A,Electrode-Box-A2,Electrode-Box-A2,Electrode-Box-A2,Electrode-Box-B2,Electrode-Box-B2,Electrode-Box-B2,Electrode-Box-B2,Left-Electrode-Cathodal,Naive,Naive,Naive,Right-Electrode
# Days Reach,Flan-1,Vu-vuong,Khoai-tay-1,Banh-mi-2,Egg-tart-2,Root-beer-2,Banh-mi-1,Egg-tart-1,Flan-2,Root-beer-1,OM-1,Khoai-lang-2,Khoai-tay-2,OM-2,Khoai-lang-1
-4,,,,,,,,,,,,,,,0
-3,,,,,,,,,,,,,,,2
-2,,,1,,,,,,,,,,,0,
-1,,,3,2,,,,,,,,,,5,0
0,,18,14,25,9,22,13,7,,11,0,0,,1,3
1,,35,22,22,2,31,35,16,,18,1,0,21,11,18
2,,61,6,22,31,49,47,23,,40,3,10,6,19,27
3,,34,28,29,44,31,65,63,,55,1,11,0,29,46
4,,61,60,27,54,60,70,69,,75,11,9,28,73,65
5,,37,75,43,84,84,102,83,,64,5,34,31,53,76
6,,49,81,74,85,,90,71,,104,9,21,62,73,79
7,,65,78,70,79,,109,79,,98,,23,87,80,81
8,,67,91,65,76,,104,98,,81,,64,105,91,98
9,,73,99,79,88,,119,89,,89,,75,75,90,101
10,,72,94,93,81,,121,96,,105,,63,101,95,103
11,,89,110,,78,,121,96,,,,59,102,60,113
12,,97,105,,96,,120,101,,,,51,95,58,117
13,,93,106,,84,,135,103,,,,73,77,,114
14,,75,91,,,,,97,,,,82,91,,119
15,,68,,,,,,,,,,70,,,111
16,,96,,,,,,,,,,76,,,74
17,,68,,,,,,,,,,76,,,109
18,,81,,,,,,,,,,63,,,88
19,,66,,,,,,,,,,48,,,98
20,,84,,,,,,,,,,65,,,118
21,,66,,,,,,,,,,75,,,109
22,,,,,,,,,,,,98,,,106
23,,,,,,,,,,,,88,,,
24,,,,,,,,,,,,94,,,
25,,,,,,,,,,,,56,,,
26,,,,,,,,,,,,75,,,
1 Data,Success
2 Group,—,—,Electrode-Box-A,Electrode-Box-A2,Electrode-Box-A2,Electrode-Box-A2,Electrode-Box-B2,Electrode-Box-B2,Electrode-Box-B2,Electrode-Box-B2,Left-Electrode-Cathodal,Naive,Naive,Naive,Right-Electrode
3 # Days Reach,Flan-1,Vu-vuong,Khoai-tay-1,Banh-mi-2,Egg-tart-2,Root-beer-2,Banh-mi-1,Egg-tart-1,Flan-2,Root-beer-1,OM-1,Khoai-lang-2,Khoai-tay-2,OM-2,Khoai-lang-1
4 -4,,,,,,,,,,,,,,,0
5 -3,,,,,,,,,,,,,,,2
6 -2,,,1,,,,,,,,,,,0,
7 -1,,,3,2,,,,,,,,,,5,0
8 0,,18,14,25,9,22,13,7,,11,0,0,,1,3
9 1,,35,22,22,2,31,35,16,,18,1,0,21,11,18
10 2,,61,6,22,31,49,47,23,,40,3,10,6,19,27
11 3,,34,28,29,44,31,65,63,,55,1,11,0,29,46
12 4,,61,60,27,54,60,70,69,,75,11,9,28,73,65
13 5,,37,75,43,84,84,102,83,,64,5,34,31,53,76
14 6,,49,81,74,85,,90,71,,104,9,21,62,73,79
15 7,,65,78,70,79,,109,79,,98,,23,87,80,81
16 8,,67,91,65,76,,104,98,,81,,64,105,91,98
17 9,,73,99,79,88,,119,89,,89,,75,75,90,101
18 10,,72,94,93,81,,121,96,,105,,63,101,95,103
19 11,,89,110,,78,,121,96,,,,59,102,60,113
20 12,,97,105,,96,,120,101,,,,51,95,58,117
21 13,,93,106,,84,,135,103,,,,73,77,,114
22 14,,75,91,,,,,97,,,,82,91,,119
23 15,,68,,,,,,,,,,70,,,111
24 16,,96,,,,,,,,,,76,,,74
25 17,,68,,,,,,,,,,76,,,109
26 18,,81,,,,,,,,,,63,,,88
27 19,,66,,,,,,,,,,48,,,98
28 20,,84,,,,,,,,,,65,,,118
29 21,,66,,,,,,,,,,75,,,109
30 22,,,,,,,,,,,,98,,,106
31 23,,,,,,,,,,,,88,,,
32 24,,,,,,,,,,,,94,,,
33 25,,,,,,,,,,,,56,,,
34 26,,,,,,,,,,,,75,,,
+35
View File
@@ -0,0 +1,35 @@
Data,Total attempts
Group,,,Electrode-Box-A,Electrode-Box-A2,Electrode-Box-A2,Electrode-Box-A2,Electrode-Box-B2,Electrode-Box-B2,Electrode-Box-B2,Electrode-Box-B2,Left-Electrode-Cathodal,Naive,Naive,Naive,Right-Electrode
# Days Reach,Flan-1,Vu-vuong,Khoai-tay-1,Banh-mi-2,Egg-tart-2,Root-beer-2,Banh-mi-1,Egg-tart-1,Flan-2,Root-beer-1,OM-1,Khoai-lang-2,Khoai-tay-2,OM-2,Khoai-lang-1
-4,,,,,,,,,,,,,,,0
-3,,,,,,,,,20,,,,,,7
-2,,,18,,,,,,,,,,,0,
-1,,,24,42,,,,,,,,,,18,0
0,,61,62,97,32,74,84,56,,85,0,0,,7,69
1,,91,83,101,38,87,86,78,,76,10,0,68,33,97
2,,127,79,119,93,134,110,103,,105,5,47,79,62,84
3,,146,97,118,101,89,140,120,,134,2,52,83,117,121
4,,141,134,136,131,140,127,132,,136,44,56,87,126,143
5,,151,138,146,139,147,142,136,,133,23,95,125,135,141
6,,131,137,146,145,,131,142,,139,32,72,144,138,146
7,,149,147,148,143,,148,138,,148,,99,141,131,153
8,,141,132,130,131,,137,142,,145,,136,152,141,144
9,,140,146,151,149,,150,139,,156,,131,148,135,149
10,,131,143,152,151,,158,143,,158,,134,149,142,150
11,,158,156,,152,,148,148,,,,139,154,133,154
12,,154,143,,155,,149,156,,,,129,144,142,148
13,,145,153,,155,,154,152,,,,143,149,,146
14,,147,152,,,,,152,,,,136,153,,132
15,,142,,,,,,,,,,145,,,156
16,,162,,,,,,,,,,135,,,156
17,,144,,,,,,,,,,150,,,139
18,,134,,,,,,,,,,122,,,138
19,,144,,,,,,,,,,116,,,140
20,,127,,,,,,,,,,134,,,153
21,,133,,,,,,,,,,131,,,146
22,,,,,,,,,,,,146,,,141
23,,,,,,,,,,,,139,,,
24,,,,,,,,,,,,148,,,
25,,,,,,,,,,,,102,,,
26,,,,,,,,,,,,143,,,
1 Data,Total attempts
2 Group,—,—,Electrode-Box-A,Electrode-Box-A2,Electrode-Box-A2,Electrode-Box-A2,Electrode-Box-B2,Electrode-Box-B2,Electrode-Box-B2,Electrode-Box-B2,Left-Electrode-Cathodal,Naive,Naive,Naive,Right-Electrode
3 # Days Reach,Flan-1,Vu-vuong,Khoai-tay-1,Banh-mi-2,Egg-tart-2,Root-beer-2,Banh-mi-1,Egg-tart-1,Flan-2,Root-beer-1,OM-1,Khoai-lang-2,Khoai-tay-2,OM-2,Khoai-lang-1
4 -4,,,,,,,,,,,,,,,0
5 -3,,,,,,,,,20,,,,,,7
6 -2,,,18,,,,,,,,,,,0,
7 -1,,,24,42,,,,,,,,,,18,0
8 0,,61,62,97,32,74,84,56,,85,0,0,,7,69
9 1,,91,83,101,38,87,86,78,,76,10,0,68,33,97
10 2,,127,79,119,93,134,110,103,,105,5,47,79,62,84
11 3,,146,97,118,101,89,140,120,,134,2,52,83,117,121
12 4,,141,134,136,131,140,127,132,,136,44,56,87,126,143
13 5,,151,138,146,139,147,142,136,,133,23,95,125,135,141
14 6,,131,137,146,145,,131,142,,139,32,72,144,138,146
15 7,,149,147,148,143,,148,138,,148,,99,141,131,153
16 8,,141,132,130,131,,137,142,,145,,136,152,141,144
17 9,,140,146,151,149,,150,139,,156,,131,148,135,149
18 10,,131,143,152,151,,158,143,,158,,134,149,142,150
19 11,,158,156,,152,,148,148,,,,139,154,133,154
20 12,,154,143,,155,,149,156,,,,129,144,142,148
21 13,,145,153,,155,,154,152,,,,143,149,,146
22 14,,147,152,,,,,152,,,,136,153,,132
23 15,,142,,,,,,,,,,145,,,156
24 16,,162,,,,,,,,,,135,,,156
25 17,,144,,,,,,,,,,150,,,139
26 18,,134,,,,,,,,,,122,,,138
27 19,,144,,,,,,,,,,116,,,140
28 20,,127,,,,,,,,,,134,,,153
29 21,,133,,,,,,,,,,131,,,146
30 22,,,,,,,,,,,,146,,,141
31 23,,,,,,,,,,,,139,,,
32 24,,,,,,,,,,,,148,,,
33 25,,,,,,,,,,,,102,,,
34 26,,,,,,,,,,,,143,,,
+57
View File
@@ -0,0 +1,57 @@
function cfg = tdcs_config()
%TDCS_CONFIG Curated group map and merge maps for the tDCS reaching study.
% CFG = TDCS_CONFIG() returns a struct with:
% .groups containers.Map, subject name (char) -> curated group name
% (char), for the 12 curated animals only. All other
% animals present in the raw export are not in this map
% and must be dropped by callers.
% .ref reference group used to code the GLME group factor,
% 'Electrode-Box-B2'.
% .mergeA2 containers.Map collapsing the two UNKNOWN conditions
% toward the weaker anchor: Electrode-Box-A ->
% Electrode-Box-A2, Right-Electrode -> Electrode-Box-B2.
% .mergeB2 containers.Map collapsing both UNKNOWN conditions onto
% the stronger anchor: Electrode-Box-A -> Electrode-Box-B2,
% Right-Electrode -> Electrode-Box-B2.
% .anchorLow 'Electrode-Box-A2', the weaker of the two known anchors.
% .anchorHigh 'Electrode-Box-B2', the stronger known anchor (== ref).
% .unknowns cell array of the two ungrouped conditions that get
% classified against the two anchors: {'Electrode-Box-A',
% 'Right-Electrode'}.
%
% See docs/superpowers/plans/2026-07-20-matlab-tdcs-analysis.md (Global
% Constraints) for the source of these values.
% Curated 12-animal group map (by animal name).
subjectGroup = {
'Vu-vuong', 'Naive'
'Khoai-lang-2', 'Naive'
'Khoai-tay-2', 'Naive'
'OM-2', 'Naive'
'Khoai-tay-1', 'Electrode-Box-A'
'Banh-mi-2', 'Electrode-Box-A2'
'Egg-tart-2', 'Electrode-Box-A2'
'Root-beer-2', 'Electrode-Box-A2'
'Banh-mi-1', 'Electrode-Box-B2'
'Egg-tart-1', 'Electrode-Box-B2'
'Root-beer-1', 'Electrode-Box-B2'
'Khoai-lang-1', 'Right-Electrode'
};
cfg = struct();
cfg.groups = containers.Map(subjectGroup(:, 1), subjectGroup(:, 2));
cfg.ref = 'Electrode-Box-B2';
cfg.anchorLow = 'Electrode-Box-A2';
cfg.anchorHigh = 'Electrode-Box-B2';
cfg.unknowns = {'Electrode-Box-A', 'Right-Electrode'};
cfg.mergeA2 = containers.Map( ...
{'Electrode-Box-A', 'Right-Electrode'}, ...
{'Electrode-Box-A2', 'Electrode-Box-B2'});
cfg.mergeB2 = containers.Map( ...
{'Electrode-Box-A', 'Right-Electrode'}, ...
{'Electrode-Box-B2', 'Electrode-Box-B2'});
end
+117
View File
@@ -0,0 +1,117 @@
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,<metric>" (skipped), line 2 blank (skipped), line 3 =
% "Group,<...>" (skipped -- grouping comes from the curated map, not
% this row), line 4 = "# Days Reach,<subject headers>" (columns 2:end
% are the subject names), lines 5+ = "<day>,<value per subject>" 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
+72
View File
@@ -0,0 +1,72 @@
classdef tLoadData < matlab.unittest.TestCase
%TLOADDATA Tests for tdcs_load_data / tdcs_config (Task 1).
%
% Authored per TDD: written before tdcs_load_data.m existed, so it is
% expected to fail until the loader is implemented. As of this
% commit MATLAB is not licensed in this environment, so this suite
% has been authored and reviewed by hand but has NOT been executed.
% Expected values (row count, group counts, load-check cells) were
% independently verified against analysis/matlab/raw/*.csv by a
% throwaway Python script (see task-1-report.md), not by running
% this test.
properties (Constant)
% Non-blank (subject, day) success cells for the 12 curated
% animals with day >= 0, counted directly from raw_success.csv.
ExpectedRowCount = 185
end
methods (Test)
function testTwelveCuratedSubjects(testCase)
T = tdcs_load_data();
testCase.verifyEqual(numel(unique(T.subject)), 12);
end
function testRowCountMatchesNonBlankDayGeq0Cells(testCase)
T = tdcs_load_data();
testCase.verifyEqual(height(T), tLoadData.ExpectedRowCount);
end
function testGroupSubjectCounts(testCase)
T = tdcs_load_data();
subjGroup = unique(T(:, {'subject', 'group'}));
counts = countcats(subjGroup.group);
catNames = categories(subjGroup.group);
expected = containers.Map( ...
{'Naive', 'Electrode-Box-A2', 'Electrode-Box-B2', ...
'Electrode-Box-A', 'Right-Electrode'}, ...
{4, 3, 3, 1, 1});
testCase.verifyEqual(numel(catNames), expected.Count);
for i = 1:numel(catNames)
name = catNames{i};
testCase.verifyTrue(isKey(expected, name), ...
sprintf('Unexpected group "%s" in loaded data.', name));
testCase.verifyEqual(counts(i), expected(name), ...
sprintf('Subject count mismatch for group "%s".', name));
end
end
function testVuVuongDayZeroLoadCheck(testCase)
T = tdcs_load_data();
row = T(T.subject == "Vu-vuong" & T.day == 0, {'success', 'total'});
testCase.verifyEqual(height(row), 1);
testCase.verifyEqual(row.success(1), 18);
testCase.verifyEqual(row.total(1), 61);
end
function testBanhMi1DayZeroLoadCheck(testCase)
T = tdcs_load_data();
row = T(T.subject == "Banh-mi-1" & T.day == 0, {'success', 'total'});
testCase.verifyEqual(height(row), 1);
testCase.verifyEqual(row.success(1), 13);
testCase.verifyEqual(row.total(1), 84);
end
function testNoNegativeDayRows(testCase)
T = tdcs_load_data();
testCase.verifyTrue(all(T.day >= 0));
end
end
end