matlab(tdcs): data loading + curated group map
This commit is contained in:
@@ -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,,,
|
||||
|
@@ -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,,,
|
||||
|
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user