how to use accumarray over a matrix avoiding a loop?
Understanding accumarray in Matlab - Stack Overflow
How to make it accumarray?
Do not understand 'accumarray' command in Maltab
I think the unexpected values are because you are expecting the values to be passed to the anonymous function in the same order as the relevant indices appears in subs.
However, the documentation for accumarray says:
Note If the subscripts in subs are not sorted, fun should not depend on the order of the values in its input data.
This means that the start of subs ( 1 2; 1 2; ) can result in 102 101 or 101 102 being passed to the anonymous function (because the documentation says that fun should not depend on the order of values.
sum(diff([102 101])) is -1 which is the value written to element 1,2 in the example
val = 101:106;
subs=[1 2; 1 2; 3 1; 4 1; 4 4; 4 1];
B = accumarray(subs,val,[],@(x)sum(diff(x)))
B =
0 -1 0 0
0 0 0 0
0 0 0 0
2 0 0 0
I'm not sure why Mathworks would choose to use the diff function in an example when the result of diff is obviously dependent on the order of values passed to it.
It looks like this input value ordering is the reason for the other output values being different from your expectations.
The why is in grantnz's answer, here's how to get predictable behavior.
As the documentation say, "If the subscripts in subs are not sorted, fun should not depend on the order of the values in its input data." So, to get predictable behavior out of accumarray, the subs have to be sorted.
What that means for subscripts representing (row,column) locations in a matrix is that the elements to which the subscripts point must be ordered according to the linear index. Thus, to "sort" 2D subscripts, you need to sort the equivalent linear index. Then you need to apply that order to vals, otherwise you have scrambled your data.
val = 101:106;
subs = [1 2; 1 2; 3 1; 4 1; 4 4; 4 1];
% convert the subscripts to linear inds and sort them
inds = sub2ind([4 4],subs(:,1),subs(:,2));
[indsSorted,sortingInds] = (sort(inds));
% apply the sorting to val
valsSorted = val(sortingInds);
% convert inds back to subs
[iiS jjS] = ind2sub([4 4],indsSorted);
subsSorted = [iiS jjS];
B = accumarray(subsSorted,valsSorted,[],@(x)sum(diff(x)))
B =
0 1 0 0
0 0 0 0
0 0 0 0
2 0 0 0
At least it would see that this is what that note in the documentation is saying.
I tried with different values for the second argument but it just seems as if the value for the second argument ends up in the last output. Can't make sense out of it. Can someone give me a hint?
Edit. I guess if I vary the first argument to [2, 8] i get more insight, but not sure whether its the right one. Then the output is a 2 by 8 matrix with the second argument being the entry for a_{2,8}. why would one want that? especially compared to the use as a group count function in the first example in its documentation: https://www.mathworks.com/help/matlab/ref/accumarray.html
For vals you can set it as 1:size(vals2,1) and use it to extract rows of vals2. Also it is required for the function to return cell.
result2 = accumarray(subs, 1:size(vals2,1), [], @(x) {prod(1+vals2(x,:),1)-1})
You can concatenate cell elements:
result3 = vertcat(result2{:})
Or all in one line:
result3 = cell2mat( accumarray(subs, 1:size(vals2,1), [], @(x) {prod(1+vals2(x,:),1)-1}))
result3 =
0.38600 0.38600
0.76635 0.76635
0.18720 0.18720
0.27008 0.27008
Result of a test in Octave comparing three proposed methods using a [10000 x 200] matrix as input:
subs = randi(1000,10000,1);
vals2 = rand(10000,200);
=========CELL2MAT========
Elapsed time is 0.130961 seconds.
=========NDGRID========
Elapsed time is 3.96383 seconds.
=========FOR LOOP========
Elapsed time is 6.16265 seconds.
Online Demo
You need to add a second set of subscripts to subs (so that it is N-by-2) to handle your 2D data, which still has to be passed as an N-element vector (i.e. one element for each row in subs). You can generate the new set of 2D subscripts using ndgrid:
[subs1, subs2] = ndgrid(subs, 1:size(vals2, 2));
result2 = accumarray([subs1(:) subs2(:)], vals2(:), [], @(x) prod(1+x) -1)
And the result with your sample data:
result2 =
0.3860 0.3860
0.7664 0.7664
0.1872 0.1872
0.2701 0.2701
One solution is to replicate the row indices in labels and add another column of column indices. Then you can reshape X into a column vector and apply accumarray once:
labels = [repmat(labels(:),nCols,1) ... % Replicate the row indices
kron(1:nCols,ones(1,numel(labels))).']; % Create column indices
totals = accumarray(labels,X(:)); % I used "totals" instead of "means"
How it works...
A = accumarray(subs,val) for a column vector subs and vector val works by adding the number in val(i) to the total in row subs(i) in the output column vector A. However, subs can contain more than just row indices. It can contain subscript indices for multiple dimensions to assign values to in the output. This feature is what allows you to handle an input val that is a matrix instead of a vector.
First, the input for val can be reshaped into a column vector using the colon operator X(:). Next, in order to keep track of which column in the output the values in X(:) should be placed, we can modify the input subs to include an additional column index. To illustrate how this works, I'll use these sample inputs:
labels = [3; 1; 1];
X = [1 2 3; ...
4 5 6; ...
7 8 9];
nCols = 3
And here are what the variables in the above code end up looking like:
labels = 3 1 X(:) = 1 totals = 11 13 15
1 1 4 0 0 0
1 1 7 1 2 3
3 2 2
1 2 5
1 2 8
3 3 3
1 3 6
1 3 9
Notice, for example, that the values 1 4 7 that were originally in the first column of X will only be accumulated in the first column of the output, as denoted by the ones in the first three rows of the second column of labels. The resulting output should be the same as what you would have gotten by using the code in the question where you loop over each column to perform the accumulation.
Perhaps a more intuitive (maybe more efficient) way borrowed from MATLAB Answers (the original answer assumes column-major inputs so I transposed them):
[xx, yy] = ndgrid(labels,1:size(X, 1));
totals = accumarray([yy(:) xx(:) ], reshape(X.', 1, []));
Example:
X = [1 2 3 4; 5 6 7 8];
labels = [2; 1; 3; 1];
gives
totals = [6 1 3; 14 5 7].
If you want to do this row-wise then there's no need to transpose, just:
[xx, yy] = ndgrid(labels,1:size(X, 2));
totals = accumarray([xx(:) yy(:)], X(:));