Smoothed Proportion of Looks Over Time (SPLOT) method
This script is based on the paper by Belopolsky (2020) and implements the SPLOT method in Matlab. This method uses cluster based permutation testing to analyze gaze data with high temporal resolution and without risking the inflation of alpha error. The script uses files (.csv) with binary gaze data over the time course to be analyzed as input. Binary data reflect dwell times or fixations directed to a specific Area of Interest (AOI). In a first step the script smoothes the binary data by means of Gaussian kernel. In a next step data is averaged across trials. Lastly, the actual cluster based permutation testing is performed.
Contents
Loading of binary data for AOI
% Select directory with .csv files for each condition seperately % containing binary gaze data over time srcGazeCond1 = uigetdir('.\SPLOT\Data\', 'Select folder for files in Condition 1'); % condition 1 (e.g. negative affect) srcGazeCond2 = uigetdir('.\SPLOT\Data\', 'Select folder for files in Condition 2'); % condition 2 (e.g. positive affect) % Loads data from the directories filesGazeCond1 = dir([srcGazeCond1, '\*.csv']); filesGazeCond2 = dir([srcGazeCond2, '\*.csv']); % Output will be saved in a subfolder of the SPLOT-folder dest = '.\SPLOT\Out\'; % Set changeable variables fps = 120; % Set resolution of data duration = 10 * fps; % Set duration of analysis sequence iterations = 1000; % Set number of iterations of permutation % Creates empty cells for the two conditions (e.g. affect types) cond1Gaze = cell(length(filesGazeCond1),1); cond2Gaze = cell(length(filesGazeCond2),1); % Warning for unequal number of files to be compared if (length(filesGazeCond1) ~= length(filesGazeCond2)) disp('Warning: Unequal number of files in folders'); end
Smoothing of binary data
% Interates over files for j=1:length(filesGazeCond1) gazeDataCond1 = readtable([srcGazeCond1,'\', filesGazeCond1(j).name]); % loads individual file for Condition 1 gazeDataCond2 = readtable([srcGazeCond2,'\', filesGazeCond2(j).name]); % loads individual file for Condition 2 % Applies gaussian kernel smoothedGazeCond1 = zeros(size(gazeDataCond1,1), size(gazeDataCond1,2)); smoothedGazeCond2 = zeros(size(gazeDataCond2,1), size(gazeDataCond2,2)); for i = 1:size(gazeDataCond1,2) smoothedGazeCond1(:,i) = conv(gazeDataCond1{:,i}, gausswin(12)/sum(gausswin(12)), 'same'); end for i = 1:size(gazeDataCond2,2) smoothedGazeCond2(:,i) = conv(gazeDataCond2{:,i}, gausswin(12)/sum(gausswin(12)), 'same'); end % Adds smoothed data to empty data container cond1Gaze{j,1} = smoothedGazeCond1; cond2Gaze{j,1} = smoothedGazeCond2; end
Creating permutation cell
% Adds all data in one matrix totalPermTrials = size(cond1Gaze{2,1},2) + size(cond2Gaze{1,1},2); permCell = cell(size(cond1Gaze,1),totalPermTrials); for j=1:size(cond1Gaze,1) for i=1:size(cond1Gaze{j,1},2) t = cond1Gaze{j,1}; permCell{j,i} = t(:,i); end if size(cond1Gaze{j,1},2) == 3 for i=(size(cond1Gaze{j,1},2)+1) + 1:(size(cond1Gaze{j,1},2)+1) + size(cond2Gaze{1,1},2) t = cond2Gaze{j,1}; permCell{j,i} = t(:,i-(size(cond1Gaze{j,1},2)+1)); end else for i=size(cond1Gaze{j,1},2) + 1:size(cond1Gaze{j,1},2) + size(cond2Gaze{1,1},2) t = cond2Gaze{j,1}; permCell{j,i} = t(:,i-size(cond1Gaze{j,1},2)); end end end
Permutation iteration
% Secures original permutation cell original = permCell; tValues = []; for k=1:iterations % Creates random permuation cell for j=1:size(original,1) t = randperm(size(original,2)); tmpCell = original(j,:); for i=1:length(t) permCell{j,i} = tmpCell{t(i)}; end end % Divides created permutation cell into two artifical conditions permCondTrials1 = permCell(:,1:floor(size(permCell,2)/2)); permCondTrials2 = permCell(:,floor(size(permCell,2)/2+1):size(permCell,2)); % Averages across trials permCond1 = averagePermTrials(permCondTrials1, duration); permCond2 = averagePermTrials(permCondTrials2, duration); % Perform paired t-test on each point in time-series [h,p,ci,stats] = ttest(permCond1,permCond2); % Defines clusters consisting of two or more adjacent significant % time points cluster = clusterValues(p, 0.05); % Defines cluster strengths [t, cl] = addTValues(cluster, stats); % Adds cluster strengths to distribution tValues = [tValues; t]; end
Defining threshold of significance ´
% Sorts t-values in distribution tValues = sort(tValues); % Gets the 95th percentil of distribution tThres = prctile(tValues,95);
Preparing & analyzing comparison of original time-series
% Divides original permutation cell orig1 = original(:,1:floor(size(original,2)/2)); orig2 = original(:,floor(size(original,2)/2+1):size(original,2)); % Averages across trials origCond1 = averagePermTrials(orig1, duration); origCond2 = averagePermTrials(orig2, duration); % Compares time points in time series between conditions by means of paired % t-tests [h,p,ci,stats] = ttest(origCond1,origCond2); % Defines clusters cluster = clusterValues(p, 0.05); % Defines cluster strengths [t, cl] = addTValues(cluster, stats); % Adds threshold to matrix cl{end + 1,1} = tThres; cl{end, 2} = "Threshold"; % Converts matrix to table and exports as .csv file clTable = array2table(cl); %writetable(clTable, [dest, 'ResultsSPLOT_face_HSA.csv']); % Save table in output folder
Ploting results
% Averages across participants avgCond1 = mean(origCond1,1); avgCond2 = mean(origCond2,1); patchX = []; for i= 1:size(cl,1)-1 if cl{i} > cl{end,1} patchX(end + 1) = cl{i,2}(1); patchX(end + 1) = cl{i,2}(end); end end % Plots time series plot(1:(duration), avgCond1(1:duration)) xlabel('Time in frames') ylabel('Look probability') hold on plot(1:(duration), avgCond2(1:duration)) axis([0,duration,0,1]) hold on % Adds shaded background for significant clusters if (size(patchX,2) == 2) patch([patchX(1) patchX(1) patchX(2) patchX(2)], [min(ylim) max(ylim) max(ylim) min(ylim)], [0.8 0.8 0.8], 'FaceAlpha', 0.5) elseif (size(patchX,2) == 4) patch([patchX(1) patchX(1) patchX(2) patchX(2)], [min(ylim) max(ylim) max(ylim) min(ylim)], [0.8 0.8 0.8], 'FaceAlpha', 0.5) hold on patch([patchX(3) patchX(3) patchX(4) patchX(4)], [min(ylim) max(ylim) max(ylim) min(ylim)], [0.8 0.8 0.8], 'FaceAlpha', 0.5) end hold on legend('Condition 1', 'Condition 2', 'Location', 'NorthEast') % Creates data container for confidence intervals lowConfCond1 = zeros(1, length(origCond1)); upperConfCond1 = zeros(1, length(origCond1)); lowConfCond2 = zeros(1, length(origCond2)); upperConfCond2 = zeros(1, length(origCond2)); % Defines confidence intervals for k =1:length(origCond1) SEM = std(origCond1(:,k))/sqrt(length(origCond1(:,k))); % Standard Error ts = tinv([0.025 0.975],length(origCond1(:,k))-1); % T-Score CI = mean(origCond1(:,k)) + ts.*SEM; % Confidence Intervals lowConfCond1(1,k) = CI(1); upperConfCond1(1,k) = CI(2); SEM = std(origCond2(:,k))/sqrt(length(origCond2(:,k))); % Standard Error ts = tinv([0.025 0.975],length(origCond2(:,k))-1); % T-Score CI = mean(origCond2(:,k)) + ts.*SEM; % Confidence Intervals lowConfCond2(1,k) = CI(1); upperConfCond2(1,k) = CI(2); end % Transforms time series data for export & adds confidence intervals to data avgCond1Table = array2table(avgCond1'); avgCond1Table.Properties.VariableNames{'Var1'} = 'Look_probability'; lowConfCond1Table = array2table(lowConfCond1'); lowConfCond1Table.Properties.VariableNames{'Var1'} = 'Lower_bound'; upperConfCond1Table = array2table(upperConfCond1'); upperConfCond1Table.Properties.VariableNames{'Var1'} = 'Upper_bound'; tableCond1 = [avgCond1Table,upperConfCond1Table, lowConfCond1Table]; avgCond2Table = array2table(avgCond2'); avgCond2Table.Properties.VariableNames{'Var1'} = 'Look_probability'; lowConfCond2Table = array2table(lowConfCond2'); lowConfCond2Table.Properties.VariableNames{'Var1'} = 'Lower_bound'; upperConfCond2Table = array2table(upperConfCond2'); upperConfCond2Table.Properties.VariableNames{'Var1'} = 'Upper_bound'; tableCond2 = [avgCond2Table, upperConfCond2Table, lowConfCond2Table]; % Exports data to .csv writetable(tableCond1, [dest, 'Timeseries_negative.csv']); writetable(tableCond2, [dest, 'Timeseries_positive.csv']);
About
- Authors: Antonia Vehlen & William Standard
- Date: 04/10/2022
- Version: 1.0
- Supplementary material for the following paper: Observed facial affect modulates gaze behavior in face-to-face interactions (Vehlen, Belopolsky & Domes, in submission)
Important functions
Averaging over trials
function [avgTrial] = averagePermTrials(trials, duration) avgTrial = zeros(size(trials,1), duration); divider = zeros(size(trials,1),1); for j=1:size(trials,1) for i=1:size(trials,2) try avgTrial(j,:) = avgTrial(j,:) + trials{j,i}'; divider(j,1) = 4; catch divider(j,1) = 3; end end avgTrial(j,:) = avgTrial(j,:) / divider(j,1); end end % Defining clusters function [cluster] = clusterValues(pArray, threshold) cluster = {}; newCluster = 1; for i=1:length(pArray) if pArray(i) < threshold if newCluster cluster{end+1} = i; else cluster{end}(end+1) = i; end newCluster = 0; else newCluster = 1; end end % Removes cluster with only one entry for i=length(cluster):-1:1 if length(cluster{i}) <= 1 cluster(i) = []; end end end % Defining cluster strength function [tValues, retCluster] = addTValues(cluster, stats) tValues = zeros(length(cluster),1); retCluster = cell(length(cluster),2); if isempty(cluster) strength = max(stats.tstat); index = find(stats.tstat==strength); retCluster{end+1,1} = strength; retCluster{end,2} = index; tValues(end+1) = abs(strength); else for i=1:length(cluster) strength = sum(stats.tstat(cluster{i})); retCluster{i,1} = strength; retCluster{i,2} = cluster{i}; tValues(i) = abs(strength); end end end