Search code examples
matlabfunctionloopsrecursionnested-loops

How to create nested loops dynamically depending on number of input arguments


I am trying to plot a data table using a function where each series is taken as a set of numbers of a certain criteria, as follows:

function plotter_fun(Table)

    aList = unique(Table.a);
    bList = unique(Table.b);
    cList = unique(Table.c);

    for a = aList
        for b = bList
            for c = cList
                Slice = Table(Table.a==a & Table.b==b & Table.c==c, :);
                plot(Slice.x, Slice.y);
            end
        end
    end
end

I want to make this so that the table header parameters ('a', 'b', 'c') can be passed in as an argument, and can be any number of parameters. The desired result would be something like:

function plotter_fun(Table, headerNames)

    for i = 1:numel(headerNames)
        loopList{i} = unique(Table.(headerNames{i}));
    end

    % do nested looping

end

Where it is called like:

plotter_fun(Table, {'a', 'b', 'c'});

I'm not sure how to have a recursive looping algorithm, such that the number of nested loops can be changed dynamically?


Solution

  • Looks like each slice is a unique combination of your header variables' values. With this in mind, we can use findgroups and unique to eliminate the loops entirely. Then the "dynamic nesting" situation isn't a problem...

    This function works as you describe:

    function plotter_fun(Table, headerNames)
        % Create a matrix of group indices for each specified header
        grps = zeros(size(Table,1), numel(headerNames));
        for i = 1:numel(headerNames)
            grps(:,i) = findgroups(Table.(headerNames{i}));
        end
        % Get the unique rows for the grouping variables
        [~, ~, idx] = unique(grps, 'rows');
        % Loop over each unique row index and slice out the specified rows.
        for i = 1:max(idx)
            Slice = Table( idx == i, : );
            plot( Slice.x, Slice.y );
        end
    end
    

    Test (this works when the plot line is removed, since I haven't specified x or y columns):

    tbl = cell2table( {'a', 1, 'dog'; 
                       'a', 2, 'cat'; 
                       'b', 3, 'cat'; 
                       'a', 2, 'cat'}, ...
                     'variablenames', {'char','num','pet'} )
    
    plotter_fun( tbl, {'char', 'num', 'pet'} ) % output slices are rows [1], [2,4] and [3].
    
    plotter_fun( tbl, {'char'} ) % output slices are rows [1,2,4] and [3].
    

    Edit:

    Here is a flexible way to auto-generate the "filter" label from the Slice. We can concatenate = between the headerNames and table values (converted using num2str in case there are numeric values), and then use strjoin to generate our label separated by , for each heading.

    The one liner looks like this, and would be used within the loop where Slice is defined:

    label = strjoin( strcat( headerNames', '=', ...
                       cellfun(@num2str,table2cell(Slice(1,headerNames)),'uni',0)' ), ', ');
    
    % Output for Slice 1 of example 1 above: 'char=a, num=1, pet=dog'
    % Output for Slice 1 of example 2 above: 'char=a'
    
    % cellfun(@num2str, __ ) conversion not needed if there is no numeric data in the table