function stat_repr = cosmo_statcode(ds, output_format)
% Convert statcode for different analysis packages
%
% stat_repr=cosmo_statcode(ds[, output_format])
%
% Inputs:
% ds dataset struct with N samples, or Nx1 cell with strings
% with statistic labels, or AFNI, BV or NIFTI header
% struct for N samples.
% output_format Optional; one of 'afni', 'bv', or 'nifti'; or empty.
%
% Returns:
% stat_repr - If output_format is empty or omitted: an Nx1 cell
% with a string representation of the statistic in each
% sample, e.g. 'Ftest(123,2)' or 'Zscore()' or empty.
% - If output_format=='afni': struct with field
% .BRICK_STATAUX.
% - If output_format=='bv': Nx1 cell with structs, each
% with fieldnames .name, .DF1 and .DF2.
% - If output_format=='nifti': struct with fieldnames
% .intent_code and .intent_p{1,2,3} if all stat codes
% are the same; empty otherwise (NIFTI does not support
% different stat codes for different samples).
%
% Notes:
% - this function is intended for fmri datasets
%
% # For CoSMoMVPA's copyright information and license terms, #
% # see the COPYING file distributed with CoSMoMVPA. #
if nargin < 2
output_format = '';
end
% see what kind of input we got
if iscellstr(ds)
% string representation of stats
stat_repr = ds(:);
elseif isstruct(ds) && isfield(ds, 'samples')
% dataset struct
if isfield(ds, 'sa') && isfield(ds.sa, 'stats')
% get stat information
stat_repr = ds.sa.stats;
else
% no stat information; return empty output
stat_repr = [];
end
else
% assume header from AFNI, BV or NIFTI; try to convert it to string
% representation, or fail in hdr2strs if that's not the case
stat_repr = hdr2strs(ds);
end
% convert to (name, df) pairs
name_df = stat_strs2name_df(stat_repr);
if isempty(output_format)
% we're done
return
end
% convert to package-specific header
stat_repr = name_df2hdr(name_df, output_format);
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% helper functions
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function name_df = stat_strs2name_df(stat_strs)
% converts string representations to name and degrees of freedom
match_regexp_failed = @(res) isempty(res) || isempty(res.name);
n = numel(stat_strs);
name_df = cell(n, 2);
for k = 1:n
stat_str = stat_strs{k};
if isempty(stat_str)
name_df{k, 1} = '';
else
% try with parentheses: 'stat_name(df1,df2,df3)'
names = regexp(stat_str, '^(?<name>\w+)\((?<df>[,\d]*)\)$', ...
'names');
if match_regexp_failed(names)
% try without parentheses: 'stat_name'
names = regexp(stat_str, '^(?<name>\w+)$', 'names');
df = [];
else
df_split = cosmo_strsplit(names.df, ',');
if isequal(df_split, {''})
df = [];
else
df = cellfun(@str2num, df_split);
end
end
if match_regexp_failed(names)
error('Unable to parse stat string %s', stat_str);
end
name_df{k, 1} = names.name;
name_df{k, 2} = df;
end
end
function hdr = name_df2hdr(name_df, output_format)
% converts statistic name and df to package-specific header
%
% hdr=name_df2hdr(name_df, output_format)
%
% Inputs:
% name_df: Kx2 cell with name and degrees of freedom, for K
% samples
% output_format 'afni', 'bv', or 'nifti'
%
% Returns:
% hdr Part of the package-specific header containing the
% statistical information
codes = get_codes(output_format);
nsamples = size(name_df, 1);
stat_idxs = zeros(nsamples, 1);
names = codes(:, 1);
for k = 1:nsamples
name = name_df{k, 1};
if isempty(name)
continue
end
idx = find(cosmo_match(names, name));
if numel(idx) == 1
% ignore if not found or multiple matches
stat_idxs(k) = idx;
elseif ~strcmp(name, 'none')
cosmo_warning('Unrecognized stat name %s at sample %d', name, k);
end
end
switch output_format
case 'afni'
hdr = struct();
% space for each sample
auxs = cell(1, nsamples);
% build BRICK_STATAUX struct
for k = 1:nsamples
stat_idx = stat_idxs(k);
if stat_idx == 0
continue
end
df = name_df{k, 2};
auxs{k} = [(k - 1) stat_idx numel(df) df];
end
% concatenate information for all samples
hdr.BRICK_STATAUX = [auxs{:}];
case 'bv'
maps = cell(nsamples, 1);
for k = 1:nsamples
map = struct();
stat_idx = stat_idxs(k);
map.Type = stat_idx;
if stat_idx ~= 0
df = name_df{k, 2};
if numel(df) >= 1
map.DF1 = df(1);
end
if numel(df) >= 2
map.DF2 = df(2);
end
end
maps{k} = map;
end
hdr = maps; % return a cell with maps
case 'nifti'
hdr = struct();
if isempty(stat_idxs)
return
end
unq_stat_idx = unique(stat_idxs);
df = name_df(:, 2);
n = numel(df);
ndf = cellfun(@numel, df);
max_ndf = max(ndf);
df_matrix = zeros(n, max_ndf);
for k = 1:n
dfk = df{k};
n_dfk = numel(dfk);
if n_dfk > 0
df_matrix(k, 1:n_dfk) = dfk;
end
end
unq_df = unique(df_matrix, 'rows');
if numel(unq_stat_idx) > 1 || numel(unique(ndf)) > 1 || ...
size(unq_df, 1) > 1
cosmo_warning(['Multiple stat codes found, unsupported '...
'by nifti']);
return
end
hdr.intent_code = unq_stat_idx;
df = zeros(1, 3);
if ~isempty(unq_df)
df(1:numel(unq_df)) = unq_df;
end
hdr.intent_p1 = df(1);
hdr.intent_p2 = df(2);
hdr.intent_p3 = df(3);
otherwise
assert(false, 'should never come here');
end
function stat_strs = hdr2strs(hdr)
% Converts package-specific header to stat string representation
%
% stat_strs=hdr2strs(hdr)
%
% Inputs:
% hdr AFNI, BV vmp, or NIFTI header
%
% Returns:
% stat_strs Kx1
if isfield(hdr, 'Map') && (isfield(hdr, 'VMRDimX') || ...
isfield(hdr, 'NrOfVertices'))
% bv vmp or smp
codes = get_codes('bv');
nsamples = numel(hdr.Map);
stat_strs = cell(nsamples, 1);
for k = 1:nsamples
map = hdr.Map(k);
tp = map.Type;
df = [map.DF1 map.DF2];
stat_strs{k} = stat_code2str(codes, tp, df);
end
elseif isstruct(hdr) && isfield(hdr, 'DATASET_RANK')
% afni
nsamples = hdr.DATASET_RANK(2);
stat_strs = repmat({''}, nsamples, 1);
if isfield(hdr, 'BRICK_STATAUX')
codes = get_codes('afni');
aux = hdr.BRICK_STATAUX;
naux = numel(aux);
pos = 0;
while (pos + 2) < naux
pos = pos + 1;
brik_idx = aux(pos) + 1; % base0 => base1
stat_code = aux(pos + 1); % stat code
n_df = aux(pos + 2); % # of df
if naux < pos + 2 + n_df
break
end
df = aux(pos + 2 + (1:n_df)); % get dfs
stat_strs{brik_idx} = stat_code2str(codes, stat_code, df);
pos = pos + 2 + n_df;
end
% sanity check
if pos ~= naux
error('Not all elements were processed in STATAUX %s', ...
sprintf('%d ', aux));
end
end
elseif isstruct(hdr) && isfield(hdr, 'dime') && isfield(hdr.dime, 'dim')
% nifti
codes = get_codes('nifti');
dime = hdr.dime;
% deal with potential data in >4th dimension
% (AFNI-NIFTI conversion syndrome)
nsamples = max(prod(dime.dim(5:end)), dime.dim(5));
% get stat codes
stat_code = dime.intent_code;
stat_df = [dime.intent_p1 dime.intent_p2 dime.intent_p3];
stat_str = stat_code2str(codes, stat_code, stat_df);
% repeat for as many samples as there are in the dataset
stat_strs = repmat({stat_str}, nsamples, 1);
else
error('Unsupported input');
end
function str = stat_code2str(codes, stat_code, df)
% give string representation of stat code with degrees of freedom
% (df can have more elements than required; superfluous ones are ignored.)
if stat_code == 0
str = '';
return
end
name = codes{stat_code, 1};
if isempty(name)
error('Illegal stat_code: %d', stat_code);
end
% build string representation of df
df_count = codes{stat_code, 2};
df_str = cellfun(@(x) sprintf('%d', x), num2cell(df(1:df_count)), ...
'UniformOutput', false);
% join with stat name
str = sprintf('%s(%s)', name, cosmo_strjoin(df_str, ','));
function codes = get_codes(package)
% get names and number of degrees of freedom for an analysis package
%
% codes=get_codes(package)
%
% Inputs:
% package 'nifti', 'afni' or 'bv'.
%
% Returns:
% codes Px2 cell, with codes{k,1} the name of the k-th stat
% and codes{k,2} the number of degrees of freedom
switch package
case 'nifti'
% http://nifti.nimh.nih.gov/nifti-1/documentation/...
% nifti_stats.pdf
codes = {'', 0; ... % not defined
'Correl', 1; ... % first one starts at 2
'Ttest', 1; ...
'Ftest', 2; ...
'Zscore', 0; ...
'Chisq', 1; ...
'Beta', 2; ...
'Binom', 2; ...
'Gamma', 2; ...
'Poisson', 1; ...
'Normal', 2; ...
'Ftest_Nonc', 3; ...
'Chisq_Nonc', 2; ...
'Logistic', 2; ...
'Laplace', 2; ...
'Uniform', 2; ...
'Ttest_Nonc', 2; ...
'Weibull', 3; ...
'Chi', 1; ...
'Invgauss', 2; ...
'Extval', 2; ...
'Pval', 0; ...
'Logpval', 0; ...
'Log10pval', 0};
case 'afni'
% http://afni.nimh.nih.gov/pub/dist/doc/program_help/...
% README.attributes.html
% AFNI uses a subset of NIFTI.
nifti_codes = get_codes('nifti'); % recursive call
codes = nifti_codes(1:10, :);
case 'bv'
% http://support.brainvoyager.com/documents/Available_Tools/...
% Available_Plugins/niftiplugin_manual_v12.pdf
% why follow a standard (like NIFTI) if one can
% come up with something incompatible too?
codes = {'Ttest', 1; ... % 1
'Correl', 1; ... % 2 correlation
'Correl', 1; ... % 3 "cross-correlation"
'Ftest', 2; ... % 4
'Zscore', 0; ... % 5
'', 0; ... % 6 [lots of undefined codes here]
'', 0; ... % 7
'', 0; ... % 8
'', 0; ... % 9
'', 0; ... % 10
'', 0; ... % 11 "percent signal change" ignored
'', 0; ... % 12 "ICA" ignored
'', 0; ... % 13
'Chisq', 1; ... % 14
'Beta', 2; ... % 15
'Pval', 0}; % 16
otherwise
error('Unsupported analysis package %s', package);
end